@wairon/cli 5.0.2-dev.8 → 5.1.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/dist/cli/index.js +2296 -824
- package/dist/cli/index.js.map +1 -1
- package/dist/index.js +1114 -137
- package/dist/index.js.map +1 -1
- package/dist/templates/skills/sdd-architect.md +1 -0
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -65,7 +65,7 @@ var init_defaults = __esm({
|
|
|
65
65
|
copilot: ".github/prompts",
|
|
66
66
|
codex: ".codex/agents"
|
|
67
67
|
};
|
|
68
|
-
WAIRON_VERSION = "5.0
|
|
68
|
+
WAIRON_VERSION = "5.1.0";
|
|
69
69
|
GITHUB_REPO = "SYW-Apps/Waffle-AIron";
|
|
70
70
|
ARCHITECT_AGENT_ID = "agent-architect";
|
|
71
71
|
ARCHITECT_TEMPLATE_ID = "architect";
|
|
@@ -458,7 +458,7 @@ var init_domain = __esm({
|
|
|
458
458
|
});
|
|
459
459
|
|
|
460
460
|
// src/models/project.ts
|
|
461
|
-
var import_zod3, BuiltinTargetConfigSchema, CustomTargetConfigSchema, TargetConfigSchema, NamingRuleConfigSchema, DocumentationRuleConfigSchema, ComplexityRuleConfigSchema, DesignDepthSchema, RulesConfigSchema, PathsConfigSchema, ProjectConfigSchema;
|
|
461
|
+
var import_zod3, BuiltinTargetConfigSchema, CustomTargetConfigSchema, TargetConfigSchema, NamingRuleConfigSchema, DocumentationRuleConfigSchema, ComplexityRuleConfigSchema, DesignDepthSchema, RulesConfigSchema, PathsConfigSchema, ProfileSelectionSubjectSchema, ProjectProfileSelectionSchema, ProjectConfigSchema;
|
|
462
462
|
var init_project = __esm({
|
|
463
463
|
"src/models/project.ts"() {
|
|
464
464
|
"use strict";
|
|
@@ -582,6 +582,24 @@ var init_project = __esm({
|
|
|
582
582
|
/** Base directory containing SDD specification files, relative to project root */
|
|
583
583
|
specsDir: import_zod3.z.string().default(".wai/specs")
|
|
584
584
|
});
|
|
585
|
+
ProfileSelectionSubjectSchema = import_zod3.z.object({
|
|
586
|
+
userId: import_zod3.z.string(),
|
|
587
|
+
kind: import_zod3.z.string(),
|
|
588
|
+
issuer: import_zod3.z.string(),
|
|
589
|
+
externalSubject: import_zod3.z.string().optional(),
|
|
590
|
+
displayName: import_zod3.z.string().optional(),
|
|
591
|
+
email: import_zod3.z.string().optional()
|
|
592
|
+
});
|
|
593
|
+
ProjectProfileSelectionSchema = import_zod3.z.object({
|
|
594
|
+
/** Selected architectural profile ids. The first resolvable one is applied as projectType. */
|
|
595
|
+
profileIds: import_zod3.z.array(import_zod3.z.string()).default([]),
|
|
596
|
+
/** Pack names the governing policy requires for this project. */
|
|
597
|
+
requiredPackNames: import_zod3.z.array(import_zod3.z.string()).default([]),
|
|
598
|
+
/** Pack names applied by default unless explicitly overridden. */
|
|
599
|
+
defaultPackNames: import_zod3.z.array(import_zod3.z.string()).optional(),
|
|
600
|
+
selectedBy: ProfileSelectionSubjectSchema.optional(),
|
|
601
|
+
selectedAt: import_zod3.z.string()
|
|
602
|
+
});
|
|
585
603
|
ProjectConfigSchema = import_zod3.z.object({
|
|
586
604
|
/**
|
|
587
605
|
* Schema version — used to detect incompatible config formats in future
|
|
@@ -622,6 +640,13 @@ var init_project = __esm({
|
|
|
622
640
|
useGlobalPacks: import_zod3.z.boolean().default(true)
|
|
623
641
|
}).optional(),
|
|
624
642
|
paths: PathsConfigSchema.default({}),
|
|
643
|
+
/**
|
|
644
|
+
* The profile/pack selection a hosted policy workflow applied to this project.
|
|
645
|
+
* The RECORD of what was chosen; `projectType` above is what actually governs
|
|
646
|
+
* validation. Modeled so the parse/write round trip preserves it (see
|
|
647
|
+
* ProjectProfileSelectionSchema).
|
|
648
|
+
*/
|
|
649
|
+
profileSelection: ProjectProfileSelectionSchema.optional(),
|
|
625
650
|
/**
|
|
626
651
|
* Path to a directory containing org/user-level default templates.
|
|
627
652
|
* Resolved before built-in templates but after project-local templates.
|
|
@@ -1095,6 +1120,18 @@ var init_specs = __esm({
|
|
|
1095
1120
|
// Required if type is 'call', references Method name on target interface
|
|
1096
1121
|
capability: import_zod6.z.string().optional(),
|
|
1097
1122
|
// Required if type is 'dispatch': the capability routed through the target Portal's dispatch table
|
|
1123
|
+
/**
|
|
1124
|
+
* call/dispatch only: the credential this step presents to an authed callee
|
|
1125
|
+
* Portal, and WHERE it is loaded from (`from`). Two forms: an OPAQUE source
|
|
1126
|
+
* (`env:API_KEY`, a config key, `vault:path`, a free note) — a design note
|
|
1127
|
+
* wairon never resolves; or a MODELED reference `component:<id>` pointing at
|
|
1128
|
+
* the Adapter/Store that provides the secret — validated to resolve, be an
|
|
1129
|
+
* Adapter/Store, and be wired to the presenter (a checked graph edge). The
|
|
1130
|
+
* actual secret is never stored here. Absence on a call into a Portal whose
|
|
1131
|
+
* `auth ≠ none` warns (PORTAL_AUTH_UNMET), so credential loading is never
|
|
1132
|
+
* overlooked.
|
|
1133
|
+
*/
|
|
1134
|
+
auth: import_zod6.z.object({ from: import_zod6.z.string(), note: import_zod6.z.string().optional() }).optional(),
|
|
1098
1135
|
assertsGuarantees: import_zod6.z.array(GuaranteeSchema).optional(),
|
|
1099
1136
|
/**
|
|
1100
1137
|
* Declared entity invariants this step upholds, as "<type-id>.<invariant-id>"
|
|
@@ -1902,6 +1939,47 @@ var init_loader = __esm({
|
|
|
1902
1939
|
}
|
|
1903
1940
|
});
|
|
1904
1941
|
|
|
1942
|
+
// src/core/statehash.ts
|
|
1943
|
+
function computeStateId() {
|
|
1944
|
+
const tree = {
|
|
1945
|
+
system: loadSystemSpec(),
|
|
1946
|
+
subsystems: loadSubsystemSpecs(),
|
|
1947
|
+
components: loadComponentSpecs(),
|
|
1948
|
+
interfaces: loadInterfaceSpecs(),
|
|
1949
|
+
implementations: loadImplementationSpecs(),
|
|
1950
|
+
types: loadTypeSpecs()
|
|
1951
|
+
};
|
|
1952
|
+
const digest = crypto.createHash("sha256").update(canonicalize(tree)).digest("hex");
|
|
1953
|
+
return { algorithm: "sha256", digest };
|
|
1954
|
+
}
|
|
1955
|
+
function stateIdEquals(a, b) {
|
|
1956
|
+
return !!a && !!b && a.algorithm === b.algorithm && a.digest === b.digest;
|
|
1957
|
+
}
|
|
1958
|
+
function canonicalize(value) {
|
|
1959
|
+
return JSON.stringify(sortKeys(value));
|
|
1960
|
+
}
|
|
1961
|
+
function sortKeys(v) {
|
|
1962
|
+
if (Array.isArray(v)) return v.map(sortKeys);
|
|
1963
|
+
if (v && typeof v === "object") {
|
|
1964
|
+
const src = v;
|
|
1965
|
+
const out = {};
|
|
1966
|
+
for (const k of Object.keys(src).sort()) {
|
|
1967
|
+
if (k === "createdAt" || k === "updatedAt") continue;
|
|
1968
|
+
out[k] = sortKeys(src[k]);
|
|
1969
|
+
}
|
|
1970
|
+
return out;
|
|
1971
|
+
}
|
|
1972
|
+
return v;
|
|
1973
|
+
}
|
|
1974
|
+
var crypto;
|
|
1975
|
+
var init_statehash = __esm({
|
|
1976
|
+
"src/core/statehash.ts"() {
|
|
1977
|
+
"use strict";
|
|
1978
|
+
crypto = __toESM(require("crypto"));
|
|
1979
|
+
init_specs2();
|
|
1980
|
+
}
|
|
1981
|
+
});
|
|
1982
|
+
|
|
1905
1983
|
// src/core/narrative-labels.ts
|
|
1906
1984
|
function resolveNarrativeLabels(methodName, steps) {
|
|
1907
1985
|
const errors = [];
|
|
@@ -2987,6 +3065,16 @@ header input[type="search"]::placeholder { color:var(--dim); }
|
|
|
2987
3065
|
#moreMenu .dropdown { display:block; width:100%; }
|
|
2988
3066
|
#moreMenu .dropdown > .tbtn, #moreMenu > .tbtn { display:block; width:100%; text-align:left; border:none; background:transparent; margin:2px 0; }
|
|
2989
3067
|
#moreMenu .dropdown > .tbtn:hover, #moreMenu > .tbtn:hover { background:var(--hover-bg); }
|
|
3068
|
+
/* Staged header compaction (responsive only, never persisted): compact
|
|
3069
|
+
stand-ins for the search input, the mode tabs, and the ancestor crumbs.
|
|
3070
|
+
All hidden at full width, so a roomy header renders exactly as before. */
|
|
3071
|
+
#searchBtn { position:relative; }
|
|
3072
|
+
#searchBtn.hasq::after { content:''; position:absolute; top:3px; right:3px; width:7px; height:7px; border-radius:50%; background:var(--accent); }
|
|
3073
|
+
.search-menu { min-width:210px; padding:8px; }
|
|
3074
|
+
.search-menu input[type="search"] { width:100%; }
|
|
3075
|
+
#modeMenu button.active { background:var(--accent); color:#fff; font-weight:700; }
|
|
3076
|
+
#crumbs .dropdown { display:inline-flex; }
|
|
3077
|
+
#crumbs .crumbmore { font-weight:700; }
|
|
2990
3078
|
|
|
2991
3079
|
/* Settings panel \u2014 toggle switches */
|
|
2992
3080
|
.settings-menu { min-width:266px; }
|
|
@@ -3110,9 +3198,17 @@ body.presentation #exitPresent, body.presentation #presentDetails { display:bloc
|
|
|
3110
3198
|
<button data-vm="types">Types</button>
|
|
3111
3199
|
<button data-vm="databases">Databases</button>
|
|
3112
3200
|
</div>
|
|
3201
|
+
<div class="dropdown" id="modeDd" style="display:none">
|
|
3202
|
+
<button class="tbtn" id="modeBtn" title="Switch between the component architecture, the type ERD, or the database schemas">Components \u25BE</button>
|
|
3203
|
+
<div class="menu" id="modeMenu"></div>
|
|
3204
|
+
</div>
|
|
3113
3205
|
<nav id="crumbs"></nav>
|
|
3114
3206
|
<span class="divider"></span>
|
|
3115
3207
|
<input id="search" type="search" placeholder="Search this view\u2026">
|
|
3208
|
+
<div class="dropdown" id="searchDd" style="display:none">
|
|
3209
|
+
<button class="tbtn" id="searchBtn" title="Search this view">\u{1F50D}</button>
|
|
3210
|
+
<div class="menu search-menu" id="searchMenu"></div>
|
|
3211
|
+
</div>
|
|
3116
3212
|
<div class="seg" id="typesDetailSeg" style="display:none" title="ERD detail level">
|
|
3117
3213
|
<button data-td="full">Full</button>
|
|
3118
3214
|
<button data-td="fields">Fields</button>
|
|
@@ -3629,13 +3725,300 @@ var MODEL = __MODEL_JSON__;
|
|
|
3629
3725
|
var BOX_W = 200, BOX_H = 56, SUBBOX_W = 230, SUBBOX_H = 84, GAP_X = 110, GAP_Y = 34;
|
|
3630
3726
|
var INNER_W = 130, INNER_H = 36, INNER_GAPX = 26, INNER_GAPY = 12, HEAD_H = 34, PADI = 14;
|
|
3631
3727
|
|
|
3728
|
+
// ---- deep expansion (opt-in via subsystem.deepInternals) -------------------
|
|
3729
|
+
// When Internals is on and a subsystem record carries deepInternals:true, its
|
|
3730
|
+
// box renders its WHOLE subtree instead of one layer: deep-flagged child
|
|
3731
|
+
// subsystems become NESTED boundary boxes (recursing, defensively capped),
|
|
3732
|
+
// every other child renders as a fixed leaf tile that is never expanded
|
|
3733
|
+
// further. Sizes are computed bottom-up (a nested container tile takes its
|
|
3734
|
+
// recursive {w,h}); positions are emitted top-down by accumulating parent
|
|
3735
|
+
// top-left offsets. Relations between concrete visible endpoints are drawn
|
|
3736
|
+
// as ONE direct line each by buildDeepContext \u2014 crossing nested boundaries
|
|
3737
|
+
// on purpose (the full org overview of project relations); only relations
|
|
3738
|
+
// that cannot resolve to two concrete endpoints keep today's port machinery,
|
|
3739
|
+
// and ONLY at the outermost box. Without the flag this whole path is inert
|
|
3740
|
+
// and the classic one-layer innerLayout runs unchanged.
|
|
3741
|
+
var DEEP_MAX_DEPTH = 6;
|
|
3742
|
+
function isDeepId(subId) {
|
|
3743
|
+
var s = subById[subId];
|
|
3744
|
+
return !!(s && s.deepInternals);
|
|
3745
|
+
}
|
|
3746
|
+
// The DEEPEST visible tile representing compId inside the deep-expanded box
|
|
3747
|
+
// rooted at rootSubId: descend deep-flagged containers (the same expansion
|
|
3748
|
+
// rule as deepContainerLayout) until the containing child is a leaf tile.
|
|
3749
|
+
// Returns the child entry { kind, id } (its node id is IN(kind, id)).
|
|
3750
|
+
function deepLeafFor(compId, rootSubId) {
|
|
3751
|
+
var subId = rootSubId, depth = 1;
|
|
3752
|
+
for (;;) {
|
|
3753
|
+
var child = childOfScopeContaining(compId, { kind: 'subsystem', id: subId });
|
|
3754
|
+
if (!child) return null;
|
|
3755
|
+
if (child.kind === 'subsystem' && isDeepId(child.id) && depth < DEEP_MAX_DEPTH) {
|
|
3756
|
+
subId = child.id; depth += 1;
|
|
3757
|
+
continue;
|
|
3758
|
+
}
|
|
3759
|
+
return child;
|
|
3760
|
+
}
|
|
3761
|
+
}
|
|
3762
|
+
// One deep container's DIRECT children, placed with PER-TILE sizes (nested
|
|
3763
|
+
// containers take their recursive size; leaves stay INNER_W x INNER_H). The
|
|
3764
|
+
// placement mirrors innerLayout's strategy switch, generalised to variable
|
|
3765
|
+
// tile sizes. Tiles are centres relative to THIS container's top-left corner
|
|
3766
|
+
// (content sits right of PADI, below the HEAD_H label band); nested tiles
|
|
3767
|
+
// are relative to their own container, so emission accumulates offsets.
|
|
3768
|
+
function deepContainerLayout(subId, depth) {
|
|
3769
|
+
var kids = childrenOf({ kind: 'subsystem', id: subId });
|
|
3770
|
+
if (!kids.length) {
|
|
3771
|
+
// An EMPTY deep subsystem still shows as a (min-size) boundary box.
|
|
3772
|
+
return { tiles: [], w: INNER_W + 2 * PADI, h: HEAD_H + PADI };
|
|
3773
|
+
}
|
|
3774
|
+
var scope = { kind: 'subsystem', id: subId };
|
|
3775
|
+
var kidKey = function (k) { return k.kind + ':' + k.id; };
|
|
3776
|
+
var size = {};
|
|
3777
|
+
kids.forEach(function (k) {
|
|
3778
|
+
if (k.kind === 'subsystem' && isDeepId(k.id) && depth < DEEP_MAX_DEPTH) {
|
|
3779
|
+
var nested = deepContainerLayout(k.id, depth + 1);
|
|
3780
|
+
size[kidKey(k)] = { w: nested.w, h: nested.h, sub: nested };
|
|
3781
|
+
} else {
|
|
3782
|
+
size[kidKey(k)] = { w: INNER_W, h: INNER_H, sub: null };
|
|
3783
|
+
}
|
|
3784
|
+
});
|
|
3785
|
+
// Intra-container edges lifted to DIRECT children \u2014 for LAYOUT ONLY (the
|
|
3786
|
+
// drawn lines come from buildDeepContext's direct pass, never per level).
|
|
3787
|
+
var intra = {};
|
|
3788
|
+
MODEL.edges.forEach(function (edge) {
|
|
3789
|
+
var a = childOfScopeContaining(edge.from, scope);
|
|
3790
|
+
var b = childOfScopeContaining(edge.to, scope);
|
|
3791
|
+
if (!a || !b) return;
|
|
3792
|
+
var ak = a.kind + ':' + a.id, bk = b.kind + ':' + b.id;
|
|
3793
|
+
if (!size[ak] || !size[bk] || ak === bk) return;
|
|
3794
|
+
intra[ak + '=>' + bk] = 1;
|
|
3795
|
+
});
|
|
3796
|
+
var layer = {};
|
|
3797
|
+
function calc(k, stack) {
|
|
3798
|
+
var key = kidKey(k);
|
|
3799
|
+
if (layer[key] !== undefined) return layer[key];
|
|
3800
|
+
if (stack[key]) return 0;
|
|
3801
|
+
stack[key] = 1;
|
|
3802
|
+
var l = 0;
|
|
3803
|
+
if (k.kind === 'component') {
|
|
3804
|
+
var c = compById[k.id];
|
|
3805
|
+
if (c && (c.componentType === 'Portal' || c.componentType === 'Observer')) { layer[key] = 0; delete stack[key]; return 0; }
|
|
3806
|
+
}
|
|
3807
|
+
Object.keys(intra).forEach(function (ek) {
|
|
3808
|
+
var cut = ek.indexOf('=>');
|
|
3809
|
+
if (ek.slice(cut + 2) !== key) return;
|
|
3810
|
+
var srcKid = kids.filter(function (x) { return kidKey(x) === ek.slice(0, cut); })[0];
|
|
3811
|
+
if (srcKid) l = Math.max(l, calc(srcKid, stack) + 1);
|
|
3812
|
+
});
|
|
3813
|
+
delete stack[key];
|
|
3814
|
+
layer[key] = l;
|
|
3815
|
+
return l;
|
|
3816
|
+
}
|
|
3817
|
+
kids.forEach(function (k) { calc(k, {}); });
|
|
3818
|
+
var tiles = [], contentW = 0, contentH = 0;
|
|
3819
|
+
if (state.layout === 'grid') {
|
|
3820
|
+
// Row packing by ACTUAL tile size (the fixed per-column grid assumed
|
|
3821
|
+
// uniform tiles); the target row width follows the tile count, widened
|
|
3822
|
+
// to at least the widest single tile.
|
|
3823
|
+
var gsorted = kids.slice().sort(function (a, b) { return a.id < b.id ? -1 : 1; });
|
|
3824
|
+
var target = Math.max(1, Math.ceil(Math.sqrt(kids.length))) * (INNER_W + INNER_GAPX);
|
|
3825
|
+
kids.forEach(function (k) { var s0 = size[kidKey(k)]; if (s0.w > target) target = s0.w; });
|
|
3826
|
+
var gx = 0, gy = 0, rowH = 0;
|
|
3827
|
+
gsorted.forEach(function (k) {
|
|
3828
|
+
var s = size[kidKey(k)];
|
|
3829
|
+
if (gx > 0 && gx + s.w > target) { gx = 0; gy += rowH + INNER_GAPY; rowH = 0; }
|
|
3830
|
+
tiles.push({ kid: k, x: gx + s.w / 2, y: gy + s.h / 2, w: s.w, h: s.h, sub: s.sub });
|
|
3831
|
+
gx += s.w + INNER_GAPX;
|
|
3832
|
+
if (s.h > rowH) rowH = s.h;
|
|
3833
|
+
if (gx - INNER_GAPX > contentW) contentW = gx - INNER_GAPX;
|
|
3834
|
+
if (gy + rowH > contentH) contentH = gy + rowH;
|
|
3835
|
+
});
|
|
3836
|
+
} else if (state.layout === 'concentric' || state.layout === 'force') {
|
|
3837
|
+
var ideg = {};
|
|
3838
|
+
kids.forEach(function (k) { ideg[kidKey(k)] = 0; });
|
|
3839
|
+
Object.keys(intra).forEach(function (ek) {
|
|
3840
|
+
var cut2 = ek.indexOf('=>');
|
|
3841
|
+
var sk = ek.slice(0, cut2), tk = ek.slice(cut2 + 2);
|
|
3842
|
+
if (ideg[sk] !== undefined) ideg[sk]++;
|
|
3843
|
+
if (ideg[tk] !== undefined) ideg[tk]++;
|
|
3844
|
+
});
|
|
3845
|
+
var rel = concentricPositions(
|
|
3846
|
+
kids.map(kidKey),
|
|
3847
|
+
function (key) { return ideg[key] || 0; },
|
|
3848
|
+
function (key) { return { w: size[key].w, h: size[key].h }; }
|
|
3849
|
+
);
|
|
3850
|
+
// Normalise by the tiles' BOUNDING BOX (not just the centres) so a wide
|
|
3851
|
+
// nested container on the rim still clears the container's left/top pad.
|
|
3852
|
+
var minL = Infinity, minT = Infinity;
|
|
3853
|
+
kids.forEach(function (k) {
|
|
3854
|
+
var s1 = size[kidKey(k)], p1 = rel[kidKey(k)] || { x: 0, y: 0 };
|
|
3855
|
+
if (p1.x - s1.w / 2 < minL) minL = p1.x - s1.w / 2;
|
|
3856
|
+
if (p1.y - s1.h / 2 < minT) minT = p1.y - s1.h / 2;
|
|
3857
|
+
});
|
|
3858
|
+
if (minL === Infinity) { minL = 0; minT = 0; }
|
|
3859
|
+
kids.forEach(function (k) {
|
|
3860
|
+
var s2 = size[kidKey(k)], p2 = rel[kidKey(k)] || { x: 0, y: 0 };
|
|
3861
|
+
var cx = p2.x - minL, cyy = p2.y - minT;
|
|
3862
|
+
tiles.push({ kid: k, x: cx, y: cyy, w: s2.w, h: s2.h, sub: s2.sub });
|
|
3863
|
+
if (cx + s2.w / 2 > contentW) contentW = cx + s2.w / 2;
|
|
3864
|
+
if (cyy + s2.h / 2 > contentH) contentH = cyy + s2.h / 2;
|
|
3865
|
+
});
|
|
3866
|
+
} else {
|
|
3867
|
+
// Layered dependency columns: the column is as wide as its widest tile,
|
|
3868
|
+
// and each tile advances by ITS OWN height.
|
|
3869
|
+
var cols = {};
|
|
3870
|
+
kids.forEach(function (k) { var l = layer[kidKey(k)] || 0; (cols[l] = cols[l] || []).push(k); });
|
|
3871
|
+
var colKeys = Object.keys(cols).map(Number).sort(function (a, b) { return a - b; });
|
|
3872
|
+
var x = 0;
|
|
3873
|
+
colKeys.forEach(function (ck) {
|
|
3874
|
+
var col = cols[ck].sort(function (a, b) { return a.id < b.id ? -1 : 1; });
|
|
3875
|
+
var colW = 0, y = 0;
|
|
3876
|
+
col.forEach(function (k) { var s3 = size[kidKey(k)]; if (s3.w > colW) colW = s3.w; });
|
|
3877
|
+
col.forEach(function (k) {
|
|
3878
|
+
var s = size[kidKey(k)];
|
|
3879
|
+
tiles.push({ kid: k, x: x + colW / 2, y: y + s.h / 2, w: s.w, h: s.h, sub: s.sub });
|
|
3880
|
+
y += s.h + INNER_GAPY;
|
|
3881
|
+
});
|
|
3882
|
+
if (y - INNER_GAPY > contentH) contentH = y - INNER_GAPY;
|
|
3883
|
+
x += colW + INNER_GAPX;
|
|
3884
|
+
});
|
|
3885
|
+
contentW = x - INNER_GAPX;
|
|
3886
|
+
}
|
|
3887
|
+
tiles.forEach(function (t) { t.x += PADI; t.y += HEAD_H; });
|
|
3888
|
+
return { tiles: tiles, w: contentW + 2 * PADI, h: HEAD_H + contentH + PADI };
|
|
3889
|
+
}
|
|
3890
|
+
// Top-level deep box: the recursive interior plus today's port machinery at
|
|
3891
|
+
// the OUTERMOST box only (buildDeepContext supplies which relations still
|
|
3892
|
+
// need ports; stubs run port <-> the DEEPEST visible leaf tile). Returns the
|
|
3893
|
+
// same shape as innerLayout, plus deep:true so emission recurses.
|
|
3894
|
+
function deepLayout(entry, portRec) {
|
|
3895
|
+
var box = deepContainerLayout(entry.id, 1);
|
|
3896
|
+
var parentId = anchorNodeId(entry);
|
|
3897
|
+
var pBaseIn = 'p~in~' + parentId + '~', pBaseOut = 'p~out~' + parentId + '~';
|
|
3898
|
+
var extIn = portRec ? portRec.extIn : {}, extOut = portRec ? portRec.extOut : {};
|
|
3899
|
+
var inIds = Object.keys(extIn).sort(), outIds = Object.keys(extOut).sort();
|
|
3900
|
+
var hasIn = inIds.length > 0, hasOut = outIds.length > 0;
|
|
3901
|
+
var PROXY_W = 22, PROXY_H = 22, PROXY_GAP = 8;
|
|
3902
|
+
var shift = hasIn ? PROXY_W + INNER_GAPX : 0;
|
|
3903
|
+
var tiles = box.tiles;
|
|
3904
|
+
if (shift) tiles.forEach(function (t) { t.x += shift; });
|
|
3905
|
+
var w = box.w + shift + (hasOut ? PROXY_W + INNER_GAPX : 0);
|
|
3906
|
+
if (w < SUBBOX_W) w = SUBBOX_W;
|
|
3907
|
+
var stackMax = Math.max(inIds.length, outIds.length);
|
|
3908
|
+
var h = Math.max(box.h, HEAD_H + stackMax * PROXY_H + Math.max(0, stackMax - 1) * PROXY_GAP + PADI);
|
|
3909
|
+
var midY = HEAD_H + Math.max(0, (h - HEAD_H - PADI) / 2);
|
|
3910
|
+
function stackPorts(ids, recs, base, cx, dir) {
|
|
3911
|
+
var total = ids.length * PROXY_H + Math.max(0, ids.length - 1) * PROXY_GAP;
|
|
3912
|
+
var y0 = Math.max(HEAD_H + PROXY_H / 2, midY - total / 2 + PROXY_H / 2);
|
|
3913
|
+
return ids.map(function (eid, i) {
|
|
3914
|
+
var km = recs[eid].kids, klist = [];
|
|
3915
|
+
Object.keys(km).forEach(function (key) { klist.push(km[key]); });
|
|
3916
|
+
return { id: base + eid, extId: eid, dir: dir, kids: klist, raws: Object.keys(recs[eid].raws), x: cx, y: y0 + i * (PROXY_H + PROXY_GAP), w: PROXY_W, h: PROXY_H };
|
|
3917
|
+
});
|
|
3918
|
+
}
|
|
3919
|
+
return {
|
|
3920
|
+
deep: true,
|
|
3921
|
+
tiles: tiles,
|
|
3922
|
+
edges: portRec ? Object.keys(portRec.stubs).map(function (k) { return portRec.stubs[k]; }) : [],
|
|
3923
|
+
proxies: stackPorts(inIds, extIn, pBaseIn, PADI + PROXY_W / 2, 'in')
|
|
3924
|
+
.concat(stackPorts(outIds, extOut, pBaseOut, w - PADI - PROXY_W / 2, 'out')),
|
|
3925
|
+
w: w,
|
|
3926
|
+
h: h,
|
|
3927
|
+
};
|
|
3928
|
+
}
|
|
3929
|
+
// View-level deep context (null unless Internals is on AND at least one
|
|
3930
|
+
// in-view entry is deep-flagged \u2014 the classic path never sees it): which
|
|
3931
|
+
// top-level entries are deep-expanded; every relation drawn as a DIRECT
|
|
3932
|
+
// concrete line (deduped per src=>tgt pair); the per-top-pair count used to
|
|
3933
|
+
// suppress aggregated edges whose constituents are ALL drawn directly; and
|
|
3934
|
+
// the port records for relations that keep today's port semantics.
|
|
3935
|
+
function buildDeepContext(scope, entries) {
|
|
3936
|
+
if (!state.internals) return null;
|
|
3937
|
+
var deepByAnchor = {}, any = false;
|
|
3938
|
+
entries.forEach(function (e) {
|
|
3939
|
+
if (e.kind === 'subsystem' && isDeepId(e.id)) { deepByAnchor[anchorNodeId(e)] = e; any = true; }
|
|
3940
|
+
});
|
|
3941
|
+
if (!any) return null;
|
|
3942
|
+
var entryByAnchor = {};
|
|
3943
|
+
entries.forEach(function (e) { entryByAnchor[e.kind + ':' + e.id] = e; });
|
|
3944
|
+
// A relation endpoint's DIRECT-line node in this view: the deepest leaf
|
|
3945
|
+
// tile inside a deep-expanded entry, or a top-level component box ITSELF.
|
|
3946
|
+
// null = this endpoint keeps aggregated/port semantics (interiors of
|
|
3947
|
+
// non-deep entries, out-of-scope counterparts).
|
|
3948
|
+
function directEnd(compId) {
|
|
3949
|
+
var child = childOfScopeContaining(compId, scope);
|
|
3950
|
+
var entry = child && entryByAnchor[child.kind + ':' + child.id];
|
|
3951
|
+
if (!entry) return null;
|
|
3952
|
+
var aid = anchorNodeId(entry);
|
|
3953
|
+
if (deepByAnchor[aid]) {
|
|
3954
|
+
var leaf = deepLeafFor(compId, entry.id);
|
|
3955
|
+
return leaf ? { node: IN(leaf.kind, leaf.id), top: aid, deep: true } : null;
|
|
3956
|
+
}
|
|
3957
|
+
if (entry.kind === 'component' && entry.id === compId) return { node: aid, top: aid, deep: false };
|
|
3958
|
+
return null;
|
|
3959
|
+
}
|
|
3960
|
+
var direct = {}, directTopCount = {}, ports = {};
|
|
3961
|
+
function portRec(aid) { return ports[aid] = ports[aid] || { extIn: {}, extOut: {}, stubs: {} }; }
|
|
3962
|
+
MODEL.edges.forEach(function (edge) {
|
|
3963
|
+
var a = directEnd(edge.from), b = directEnd(edge.to);
|
|
3964
|
+
if (a && b && (a.deep || b.deep) && a.node !== b.node) {
|
|
3965
|
+
// Drawn as ONE direct line \u2014 never ALSO as ports/stubs (dedupe rule).
|
|
3966
|
+
var key = a.node + '=>' + b.node;
|
|
3967
|
+
if (!direct[key]) direct[key] = { src: a.node, tgt: b.node, cross: false, aTop: a.top, bTop: b.top };
|
|
3968
|
+
if (edge.cross) direct[key].cross = true;
|
|
3969
|
+
if (a.top !== b.top) {
|
|
3970
|
+
var tk = a.top + '=>' + b.top;
|
|
3971
|
+
directTopCount[tk] = (directTopCount[tk] || 0) + 1;
|
|
3972
|
+
}
|
|
3973
|
+
return;
|
|
3974
|
+
}
|
|
3975
|
+
// Not a direct line: keep today's port semantics on any deep box with
|
|
3976
|
+
// exactly one endpoint inside its subtree, stubbed to the deepest leaf.
|
|
3977
|
+
var ac = childOfScopeContaining(edge.from, scope);
|
|
3978
|
+
var bc = childOfScopeContaining(edge.to, scope);
|
|
3979
|
+
var aEnt = ac && entryByAnchor[ac.kind + ':' + ac.id];
|
|
3980
|
+
var bEnt = bc && entryByAnchor[bc.kind + ':' + bc.id];
|
|
3981
|
+
var aAid = aEnt ? anchorNodeId(aEnt) : null;
|
|
3982
|
+
var bAid = bEnt ? anchorNodeId(bEnt) : null;
|
|
3983
|
+
if (aAid === bAid) return; // internal to one entry, or neither in scope
|
|
3984
|
+
if (aAid && deepByAnchor[aAid]) {
|
|
3985
|
+
var leafA = deepLeafFor(edge.from, aEnt.id);
|
|
3986
|
+
if (leafA) {
|
|
3987
|
+
var recA = portRec(aAid);
|
|
3988
|
+
var ro = recA.extOut[edge.to] = recA.extOut[edge.to] || { kids: {}, raws: {} };
|
|
3989
|
+
ro.kids[leafA.kind + ':' + leafA.id] = leafA;
|
|
3990
|
+
ro.raws[edge.from] = 1;
|
|
3991
|
+
var poId = 'p~out~' + aAid + '~' + edge.to;
|
|
3992
|
+
recA.stubs[IN(leafA.kind, leafA.id) + '=>' + poId] = { src: IN(leafA.kind, leafA.id), tgt: poId, stub: true };
|
|
3993
|
+
}
|
|
3994
|
+
}
|
|
3995
|
+
if (bAid && deepByAnchor[bAid]) {
|
|
3996
|
+
var leafB = deepLeafFor(edge.to, bEnt.id);
|
|
3997
|
+
if (leafB) {
|
|
3998
|
+
var recB = portRec(bAid);
|
|
3999
|
+
var ri = recB.extIn[edge.from] = recB.extIn[edge.from] || { kids: {}, raws: {} };
|
|
4000
|
+
ri.kids[leafB.kind + ':' + leafB.id] = leafB;
|
|
4001
|
+
ri.raws[edge.to] = 1;
|
|
4002
|
+
var piId = 'p~in~' + bAid + '~' + edge.from;
|
|
4003
|
+
recB.stubs[piId + '=>' + IN(leafB.kind, leafB.id)] = { src: piId, tgt: IN(leafB.kind, leafB.id), stub: true };
|
|
4004
|
+
}
|
|
4005
|
+
}
|
|
4006
|
+
});
|
|
4007
|
+
return { deepByAnchor: deepByAnchor, direct: direct, directTopCount: directTopCount, ports: ports };
|
|
4008
|
+
}
|
|
4009
|
+
|
|
3632
4010
|
// Micro-layout for a container's direct children when Internals is on:
|
|
3633
4011
|
// layered mini columns + intra-container edges. Each external relation gets
|
|
3634
4012
|
// its own small PORT node INSIDE the container (one per external
|
|
3635
4013
|
// counterpart; incoming left, outgoing right). Children connect to ports
|
|
3636
4014
|
// with short edges that never leave the box \u2014 the real cross-boundary line
|
|
3637
4015
|
// is only revealed on hover, or pinned while the port is selected.
|
|
3638
|
-
function innerLayout(entry) {
|
|
4016
|
+
function innerLayout(entry, deepCtx) {
|
|
4017
|
+
// Deep-flagged subsystems take the recursive path (deepCtx exists only
|
|
4018
|
+
// when Internals is on and the view has deep entries \u2014 see buildElements).
|
|
4019
|
+
if (deepCtx && entry.kind === 'subsystem' && deepCtx.deepByAnchor[anchorNodeId(entry)]) {
|
|
4020
|
+
return deepLayout(entry, deepCtx.ports[anchorNodeId(entry)]);
|
|
4021
|
+
}
|
|
3639
4022
|
var kids = state.internals && entry.hasKids ? childrenOf({ kind: entry.kind, id: entry.id }) : [];
|
|
3640
4023
|
if (!kids.length) return null;
|
|
3641
4024
|
var scope = { kind: entry.kind, id: entry.id };
|
|
@@ -4207,12 +4590,42 @@ var MODEL = __MODEL_JSON__;
|
|
|
4207
4590
|
if (scope.kind === 'types' || scope.kind === 'databases') return buildTypeElements();
|
|
4208
4591
|
var entries = childrenOf(scope);
|
|
4209
4592
|
var eles = [];
|
|
4593
|
+
var deepCtx = buildDeepContext(scope, entries);
|
|
4210
4594
|
var ve = viewEdges(scope, entries);
|
|
4211
4595
|
// Data-coupling overlay: same scoping pipeline, a different edge source.
|
|
4212
4596
|
var vd = state.dataCoupling ? viewEdges(scope, entries, MODEL.dataEdges) : { agg: {}, ghosts: {} };
|
|
4213
4597
|
Object.keys(vd.ghosts).forEach(function (g) { if (!ve.ghosts[g]) ve.ghosts[g] = vd.ghosts[g]; });
|
|
4214
4598
|
var inners = {};
|
|
4215
|
-
entries.forEach(function (e) { inners[anchorNodeId(e)] = innerLayout(e); });
|
|
4599
|
+
entries.forEach(function (e) { inners[anchorNodeId(e)] = innerLayout(e, deepCtx); });
|
|
4600
|
+
|
|
4601
|
+
// Deep-mode recursive tile emission: a nested container becomes a cytoscape
|
|
4602
|
+
// compound parent (no explicit position \u2014 a compound derives its bounds
|
|
4603
|
+
// from its children); leaves and EMPTY containers are plain positioned
|
|
4604
|
+
// nodes. ox/oy = the emitting container's absolute top-left; tile.x/y are
|
|
4605
|
+
// centres relative to it, so offsets accumulate top-down. Search dimming
|
|
4606
|
+
// propagates the TOP entry's dim to the whole subtree.
|
|
4607
|
+
function emitDeepTiles(tiles, parentNodeId, ox, oy, dimCls) {
|
|
4608
|
+
tiles.forEach(function (tile) {
|
|
4609
|
+
var ax = ox + tile.x, ay = oy + tile.y;
|
|
4610
|
+
if (tile.sub) {
|
|
4611
|
+
var nid = SN(tile.kid.id);
|
|
4612
|
+
var selCls = state.selectedKind === 'subsystem' && state.selected === tile.kid.id ? ' sel' : '';
|
|
4613
|
+
var nested = {
|
|
4614
|
+
data: { id: nid, parent: parentNodeId, label: nameOf(tile.kid), w: tile.w, h: tile.h, tw: tile.w - 16 },
|
|
4615
|
+
classes: 'subsysBox' + (tile.sub.tiles.length ? ' drillable' : '') + dimCls + selCls,
|
|
4616
|
+
};
|
|
4617
|
+
if (!tile.sub.tiles.length) nested.position = { x: ax, y: ay };
|
|
4618
|
+
eles.push(nested);
|
|
4619
|
+
emitDeepTiles(tile.sub.tiles, nid, ax - tile.w / 2, ay - tile.h / 2, dimCls);
|
|
4620
|
+
} else {
|
|
4621
|
+
eles.push({
|
|
4622
|
+
data: { id: IN(tile.kid.kind, tile.kid.id), parent: parentNodeId, label: nameOf(tile.kid), w: INNER_W, h: INNER_H, tw: INNER_W - 10 },
|
|
4623
|
+
position: { x: ax, y: ay },
|
|
4624
|
+
classes: 'inner' + dimCls,
|
|
4625
|
+
});
|
|
4626
|
+
}
|
|
4627
|
+
});
|
|
4628
|
+
}
|
|
4216
4629
|
|
|
4217
4630
|
// Resolve a port's reveal target(s) in THIS view. Preference order: the
|
|
4218
4631
|
// MATCHING PORT inside the counterpart's container (a port-to-port line
|
|
@@ -4344,17 +4757,25 @@ var MODEL = __MODEL_JSON__;
|
|
|
4344
4757
|
+ (state.showIssues && issuesBySpec[e.id] ? ' hasIssue' : '')
|
|
4345
4758
|
+ (state.selectedKind === e.kind && state.selected === e.id ? ' sel' : '');
|
|
4346
4759
|
if (inner) {
|
|
4347
|
-
|
|
4348
|
-
|
|
4349
|
-
|
|
4350
|
-
|
|
4351
|
-
|
|
4352
|
-
|
|
4353
|
-
|
|
4354
|
-
|
|
4355
|
-
|
|
4760
|
+
var boxNode = { data: { id: aid, label: e.kind === 'subsystem' ? nameOf(e) : nameOf(e), w: p.w, h: p.h, tw: p.w - 16 }, classes: classes };
|
|
4761
|
+
// An EMPTY deep boundary box has no children, so it is NOT a compound
|
|
4762
|
+
// parent \u2014 it needs (and honours) an explicit position and size.
|
|
4763
|
+
if (inner.deep && !inner.tiles.length && !(inner.proxies && inner.proxies.length)) boxNode.position = { x: p.x, y: p.y };
|
|
4764
|
+
eles.push(boxNode);
|
|
4765
|
+
if (inner.deep) {
|
|
4766
|
+
emitDeepTiles(inner.tiles, aid, p.x - p.w / 2, p.y - p.h / 2, dim ? ' dimmed' : '');
|
|
4767
|
+
} else {
|
|
4768
|
+
inner.tiles.forEach(function (tile) {
|
|
4769
|
+
eles.push({
|
|
4770
|
+
data: {
|
|
4771
|
+
id: IN(tile.kid.kind, tile.kid.id), parent: aid,
|
|
4772
|
+
label: nameOf(tile.kid), w: INNER_W, h: INNER_H, tw: INNER_W - 10,
|
|
4773
|
+
},
|
|
4774
|
+
position: { x: p.x - p.w / 2 + tile.x, y: p.y - p.h / 2 + tile.y },
|
|
4775
|
+
classes: 'inner' + (dim ? ' dimmed' : ''),
|
|
4776
|
+
});
|
|
4356
4777
|
});
|
|
4357
|
-
}
|
|
4778
|
+
}
|
|
4358
4779
|
(inner.proxies || []).forEach(function (px) {
|
|
4359
4780
|
eles.push({
|
|
4360
4781
|
data: {
|
|
@@ -4507,9 +4928,28 @@ var MODEL = __MODEL_JSON__;
|
|
|
4507
4928
|
|
|
4508
4929
|
var dimmedAnchors = {};
|
|
4509
4930
|
entries.forEach(function (e) { if (state.query && !matches(e)) dimmedAnchors[anchorNodeId(e)] = true; });
|
|
4931
|
+
|
|
4932
|
+
// Deep mode: ONE direct line per related pair of concrete visible nodes \u2014
|
|
4933
|
+
// leaf tiles at any depth and/or top-level component boxes (deduped by
|
|
4934
|
+
// buildDeepContext). These lines cross nested boundaries on purpose.
|
|
4935
|
+
if (deepCtx) {
|
|
4936
|
+
var ddi = 0;
|
|
4937
|
+
Object.keys(deepCtx.direct).sort().forEach(function (key) {
|
|
4938
|
+
var d = deepCtx.direct[key];
|
|
4939
|
+
var ddim = state.query && (dimmedAnchors[d.aTop] || dimmedAnchors[d.bTop]);
|
|
4940
|
+
eles.push({
|
|
4941
|
+
data: { id: 'dd' + (ddi++), source: d.src, target: d.tgt, lbl: '' },
|
|
4942
|
+
classes: 'inneredge' + (d.cross ? ' cross' : '') + (ddim ? ' dimmed' : ''),
|
|
4943
|
+
});
|
|
4944
|
+
});
|
|
4945
|
+
}
|
|
4946
|
+
|
|
4510
4947
|
var i = 0;
|
|
4511
4948
|
Object.keys(ve.agg).forEach(function (key) {
|
|
4512
4949
|
var e = ve.agg[key];
|
|
4950
|
+
// Prefer the leaf lines: drop an aggregated container edge whose
|
|
4951
|
+
// constituent relations were ALL drawn as direct deep lines above.
|
|
4952
|
+
if (deepCtx && deepCtx.directTopCount[key] >= e.n) return;
|
|
4513
4953
|
var bundle = e.n > 1;
|
|
4514
4954
|
var dim = state.query && (dimmedAnchors[e.src] || dimmedAnchors[e.tgt]);
|
|
4515
4955
|
var route = routeData(e.src, e.tgt, key);
|
|
@@ -4768,22 +5208,58 @@ var MODEL = __MODEL_JSON__;
|
|
|
4768
5208
|
}
|
|
4769
5209
|
return path;
|
|
4770
5210
|
}
|
|
5211
|
+
// Crumb compaction (compaction stage 5) is a RENDER MODE, not marker-based
|
|
5212
|
+
// reparenting: renderCrumbs rebuilds #crumbs' innerHTML on every navigation,
|
|
5213
|
+
// so nodes physically moved elsewhere would be destroyed by the next render.
|
|
5214
|
+
// The header-compaction stage toggles crumbsCompact and re-renders; compact
|
|
5215
|
+
// keeps the CURRENT scope visible and folds the ancestors into an ordered
|
|
5216
|
+
// "\\u2026" dropdown (document order, root first) whose entries navigate
|
|
5217
|
+
// exactly like the crumbs they replace.
|
|
5218
|
+
var crumbsCompact = false;
|
|
5219
|
+
var lastCrumbsHtml; // no initializer: the boot render at cy-init time precedes this line
|
|
5220
|
+
// Set by the header-compaction IIFE: crumb re-renders change the header's
|
|
5221
|
+
// CONTENT width without resizing #hdr itself (it is edge-anchored), so the
|
|
5222
|
+
// ResizeObserver never fires for them \u2014 renderCrumbs nudges a reflow here.
|
|
5223
|
+
var headerReflowHook = null;
|
|
5224
|
+
function crumbBtnHtml(p, cur) {
|
|
5225
|
+
return '<button class="crumb' + (cur ? ' cur' : '') + '" data-ck="' + p.kind + '" data-ci="' + (p.id || '') + '">' + p.label + '</button>';
|
|
5226
|
+
}
|
|
4771
5227
|
function renderCrumbs() {
|
|
4772
5228
|
var el = document.getElementById('crumbs');
|
|
4773
5229
|
var path = crumbPath();
|
|
4774
|
-
|
|
4775
|
-
|
|
4776
|
-
|
|
4777
|
-
+
|
|
4778
|
-
|
|
5230
|
+
var html;
|
|
5231
|
+
if (crumbsCompact && path.length > 1) {
|
|
5232
|
+
html = '<span class="dropdown" id="crumbDd">'
|
|
5233
|
+
+ '<button class="crumb crumbmore" id="crumbMoreBtn" title="Show the collapsed ancestor path">\\u2026</button>'
|
|
5234
|
+
+ '<span class="menu" id="crumbMenu">'
|
|
5235
|
+
+ path.slice(0, path.length - 1).map(function (p) {
|
|
5236
|
+
return '<button data-ck="' + p.kind + '" data-ci="' + (p.id || '') + '">' + p.label + '</button>';
|
|
5237
|
+
}).join('')
|
|
5238
|
+
+ '</span></span>'
|
|
5239
|
+
+ '<span class="sep">\\u203A</span>'
|
|
5240
|
+
+ crumbBtnHtml(path[path.length - 1], true);
|
|
5241
|
+
} else {
|
|
5242
|
+
html = path.map(function (p, i) {
|
|
5243
|
+
var cur = i === path.length - 1;
|
|
5244
|
+
return crumbBtnHtml(p, cur) + (cur ? '' : '<span class="sep">\\u203A</span>');
|
|
5245
|
+
}).join('');
|
|
5246
|
+
}
|
|
5247
|
+
// No-op renders keep the already-wired nodes (and an open "\\u2026" menu)
|
|
5248
|
+
// intact \u2014 and don't churn the reflow scheduler while a search query types.
|
|
5249
|
+
if (html === lastCrumbsHtml) return;
|
|
5250
|
+
lastCrumbsHtml = html;
|
|
5251
|
+
el.innerHTML = html;
|
|
4779
5252
|
var btns = el.querySelectorAll('button');
|
|
4780
5253
|
for (var i = 0; i < btns.length; i++) {
|
|
4781
5254
|
(function (b) {
|
|
5255
|
+
if (!b.getAttribute('data-ck')) return; // the "\\u2026" trigger toggles, never navigates
|
|
4782
5256
|
b.addEventListener('click', function () {
|
|
4783
5257
|
navigateTo(b.getAttribute('data-ck'), b.getAttribute('data-ci') || null);
|
|
4784
5258
|
});
|
|
4785
5259
|
})(btns[i]);
|
|
4786
5260
|
}
|
|
5261
|
+
if (crumbsCompact && path.length > 1) wireDropdown('crumbDd', 'crumbMoreBtn');
|
|
5262
|
+
if (headerReflowHook) headerReflowHook();
|
|
4787
5263
|
}
|
|
4788
5264
|
function renderViewHint() {
|
|
4789
5265
|
if (state.view.kind === 'types' || state.view.kind === 'databases') {
|
|
@@ -5050,7 +5526,16 @@ var MODEL = __MODEL_JSON__;
|
|
|
5050
5526
|
return c ? { kind: 'subsystem', id: c.subsystem } : { kind: 'system', id: null };
|
|
5051
5527
|
}
|
|
5052
5528
|
|
|
5053
|
-
|
|
5529
|
+
// While the search input is compacted behind the magnifier icon (compaction
|
|
5530
|
+
// stage 3), a non-empty query must stay discoverable \u2014 mark the icon with an
|
|
5531
|
+
// accent dot. The class is kept in sync on every query edit; the dot is only
|
|
5532
|
+
// ever visible while the compact icon itself is.
|
|
5533
|
+
function updateSearchBadge() {
|
|
5534
|
+
var b = document.getElementById('searchBtn');
|
|
5535
|
+
if (b && b.classList) b.classList[state.query ? 'add' : 'remove']('hasq');
|
|
5536
|
+
}
|
|
5537
|
+
document.getElementById('search').addEventListener('input', function (ev) { state.query = ev.target.value.trim(); updateSearchBadge(); rebuild(false); });
|
|
5538
|
+
updateSearchBadge();
|
|
5054
5539
|
// Sync each View toggle's checkbox from the (possibly persisted) state, then
|
|
5055
5540
|
// persist on change so the choices survive a refresh (see persist()/saved).
|
|
5056
5541
|
document.getElementById('internalsToggle').checked = state.internals;
|
|
@@ -5104,9 +5589,24 @@ var MODEL = __MODEL_JSON__;
|
|
|
5104
5589
|
})(btns[i]);
|
|
5105
5590
|
}
|
|
5106
5591
|
})();
|
|
5592
|
+
// Compaction stage 4 replaces the mode tabs with one dropdown trigger; its
|
|
5593
|
+
// label must follow the CURRENT mode. updateHeaderSegs runs on every rebuild,
|
|
5594
|
+
// so a mode change made while compact re-labels the trigger immediately.
|
|
5595
|
+
function updateModeBtn() {
|
|
5596
|
+
var b = document.getElementById('modeBtn');
|
|
5597
|
+
if (!b) return;
|
|
5598
|
+
var lbl = state.view.kind === 'types' ? 'Types' : state.view.kind === 'databases' ? 'Databases' : 'Components';
|
|
5599
|
+
b.textContent = lbl + ' \\u25BE';
|
|
5600
|
+
}
|
|
5107
5601
|
function updateHeaderSegs() {
|
|
5108
5602
|
var seg = document.getElementById('modeSeg');
|
|
5109
5603
|
var btns = seg.querySelectorAll('button');
|
|
5604
|
+
if (!btns.length) {
|
|
5605
|
+
// Compaction stage 4 moved the real tab buttons into the mode dropdown \u2014
|
|
5606
|
+
// keep driving THEIR active classes there (they move back node-identical).
|
|
5607
|
+
var mm = document.getElementById('modeMenu');
|
|
5608
|
+
if (mm && mm.querySelectorAll) btns = mm.querySelectorAll('button');
|
|
5609
|
+
}
|
|
5110
5610
|
for (var i = 0; i < btns.length; i++) {
|
|
5111
5611
|
var vm = btns[i].getAttribute('data-vm');
|
|
5112
5612
|
var active = vm === 'components'
|
|
@@ -5114,6 +5614,7 @@ var MODEL = __MODEL_JSON__;
|
|
|
5114
5614
|
: vm === state.view.kind;
|
|
5115
5615
|
if (btns[i].classList) btns[i].classList[active ? 'add' : 'remove']('active');
|
|
5116
5616
|
}
|
|
5617
|
+
updateModeBtn();
|
|
5117
5618
|
var td = document.getElementById('typesDetailSeg');
|
|
5118
5619
|
td.style.display = (state.view.kind === 'types' || state.view.kind === 'databases') ? '' : 'none';
|
|
5119
5620
|
var tbs = td.querySelectorAll('button');
|
|
@@ -5177,7 +5678,13 @@ var MODEL = __MODEL_JSON__;
|
|
|
5177
5678
|
var r = btn.getBoundingClientRect();
|
|
5178
5679
|
menu.style.top = (r.bottom + 6) + 'px';
|
|
5179
5680
|
menu.style.left = 'auto';
|
|
5180
|
-
|
|
5681
|
+
// Right-aligned to the trigger, but never pushed off the LEFT edge \u2014 the
|
|
5682
|
+
// compact search / crumb triggers (header compaction) sit on the header's
|
|
5683
|
+
// left side, where a 200px menu right-aligned to a narrow button would clip.
|
|
5684
|
+
var right = Math.max(6, window.innerWidth - r.right);
|
|
5685
|
+
var mw = menu.getBoundingClientRect ? menu.getBoundingClientRect().width : 0;
|
|
5686
|
+
if (mw && window.innerWidth - right - mw < 6) right = Math.max(6, window.innerWidth - mw - 6);
|
|
5687
|
+
menu.style.right = right + 'px';
|
|
5181
5688
|
}
|
|
5182
5689
|
function wireDropdown(ddId, btnId) {
|
|
5183
5690
|
var dd = document.getElementById(ddId);
|
|
@@ -5196,11 +5703,29 @@ var MODEL = __MODEL_JSON__;
|
|
|
5196
5703
|
var ldd = wireDropdown('layoutDd', 'layoutBtn');
|
|
5197
5704
|
var sdd = wireDropdown('settingsDd', 'settingsBtn');
|
|
5198
5705
|
var mdd = wireDropdown('moreDd', 'moreBtn');
|
|
5706
|
+
// Compact stand-ins (header compaction stages 3-4): the search panel and the
|
|
5707
|
+
// mode-tab dropdown are ordinary dropdowns; their triggers stay hidden until
|
|
5708
|
+
// their compaction stage shows them, so wiring them here is inert at full width.
|
|
5709
|
+
var qdd = wireDropdown('searchDd', 'searchBtn');
|
|
5710
|
+
var vdd = wireDropdown('modeDd', 'modeBtn');
|
|
5711
|
+
// Opening the compact search panel focuses the REAL input (stage 3 moves the
|
|
5712
|
+
// node, never clones it, so its input listener keeps driving state.query).
|
|
5713
|
+
// Registered after wireDropdown's toggle, so 'open' reflects the new state.
|
|
5714
|
+
document.getElementById('searchBtn').addEventListener('click', function () {
|
|
5715
|
+
if (String(qdd.className || '').indexOf('open') >= 0) {
|
|
5716
|
+
var inp = document.getElementById('search');
|
|
5717
|
+
if (inp && inp.focus) inp.focus();
|
|
5718
|
+
}
|
|
5719
|
+
});
|
|
5199
5720
|
// Keep the settings panel open while flipping switches (clicks inside it don't
|
|
5200
5721
|
// bubble to the document-level close handler).
|
|
5201
5722
|
(function () {
|
|
5202
5723
|
var m = document.getElementById('settingsMenu');
|
|
5203
5724
|
if (m && m.addEventListener) m.addEventListener('click', function (ev) { if (ev && ev.stopPropagation) ev.stopPropagation(); });
|
|
5725
|
+
// Same for the floating search panel: clicking into the input must not
|
|
5726
|
+
// bubble to the document-level close handler and shut the panel mid-typing.
|
|
5727
|
+
var sm = document.getElementById('searchMenu');
|
|
5728
|
+
if (sm && sm.addEventListener) sm.addEventListener('click', function (ev) { if (ev && ev.stopPropagation) ev.stopPropagation(); });
|
|
5204
5729
|
})();
|
|
5205
5730
|
|
|
5206
5731
|
// Layout picker: choose the auto-layout algorithm. Components use cytoscape's
|
|
@@ -5224,6 +5749,12 @@ var MODEL = __MODEL_JSON__;
|
|
|
5224
5749
|
});
|
|
5225
5750
|
updateLayoutBtn();
|
|
5226
5751
|
|
|
5752
|
+
// The compact crumb dropdown (compaction stage 5) is re-created by every
|
|
5753
|
+
// compact crumb render, so it is looked up per close instead of captured.
|
|
5754
|
+
function closeCrumbDd() {
|
|
5755
|
+
var cdd = document.getElementById('crumbDd');
|
|
5756
|
+
if (cdd && cdd.classList) cdd.classList.remove('open');
|
|
5757
|
+
}
|
|
5227
5758
|
if (document.addEventListener) {
|
|
5228
5759
|
document.addEventListener('click', function () {
|
|
5229
5760
|
if (dd.classList) dd.classList.remove('open');
|
|
@@ -5231,6 +5762,9 @@ var MODEL = __MODEL_JSON__;
|
|
|
5231
5762
|
if (ldd.classList) ldd.classList.remove('open');
|
|
5232
5763
|
if (sdd.classList) sdd.classList.remove('open');
|
|
5233
5764
|
if (mdd && mdd.classList) mdd.classList.remove('open');
|
|
5765
|
+
if (qdd && qdd.classList) qdd.classList.remove('open');
|
|
5766
|
+
if (vdd && vdd.classList) vdd.classList.remove('open');
|
|
5767
|
+
closeCrumbDd();
|
|
5234
5768
|
});
|
|
5235
5769
|
document.addEventListener('keydown', function (ev) {
|
|
5236
5770
|
if (ev.key === 'Escape') {
|
|
@@ -5238,16 +5772,27 @@ var MODEL = __MODEL_JSON__;
|
|
|
5238
5772
|
if (modal.classList && String(modal.className).indexOf('open') >= 0) { closeFlow(); return; }
|
|
5239
5773
|
setPresentation(false);
|
|
5240
5774
|
if (dd.classList) dd.classList.remove('open');
|
|
5775
|
+
if (qdd && qdd.classList) qdd.classList.remove('open');
|
|
5776
|
+
if (vdd && vdd.classList) vdd.classList.remove('open');
|
|
5777
|
+
closeCrumbDd();
|
|
5241
5778
|
}
|
|
5242
5779
|
});
|
|
5243
5780
|
}
|
|
5244
5781
|
|
|
5245
|
-
// \u2500\u2500 Responsive header
|
|
5246
|
-
// When the floating header no longer fits its controls,
|
|
5247
|
-
//
|
|
5248
|
-
//
|
|
5249
|
-
//
|
|
5250
|
-
//
|
|
5782
|
+
// \u2500\u2500 Responsive header compaction \u2192 ordered stages \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
5783
|
+
// When the floating header no longer fits its controls, standard reusable
|
|
5784
|
+
// COMPACTION BEHAVIORS apply progressively \u2014 each stage only while the row
|
|
5785
|
+
// still overflows \u2014 and restore in REVERSE order when space returns:
|
|
5786
|
+
// 1. trailing buttons fold into the "\u22EF" menu (least-used first, one by one)
|
|
5787
|
+
// 2. the View dropdown folds in after them
|
|
5788
|
+
// 3. the search input compacts to a \u{1F50D} icon + floating panel
|
|
5789
|
+
// 4. the mode tabs compact to one current-mode dropdown
|
|
5790
|
+
// 5. ancestor crumbs compact into an ordered "\u2026" dropdown
|
|
5791
|
+
// Stages 1-2 move whole items (listeners survive reparenting) with a hidden
|
|
5792
|
+
// placeholder pinning each item's original spot for restore; stage 5 is a
|
|
5793
|
+
// render mode (renderCrumbs rebuilds its innerHTML, so reparenting would not
|
|
5794
|
+
// survive navigation). Nothing here is persisted \u2014 compaction is purely
|
|
5795
|
+
// responsive to the available width.
|
|
5251
5796
|
(function () {
|
|
5252
5797
|
if (typeof window === 'undefined') return;
|
|
5253
5798
|
var hdr = document.getElementById('hdr');
|
|
@@ -5275,7 +5820,122 @@ var MODEL = __MODEL_JSON__;
|
|
|
5275
5820
|
}
|
|
5276
5821
|
return markers[id];
|
|
5277
5822
|
}
|
|
5278
|
-
|
|
5823
|
+
// Fold stage (the classic behavior): move items into the "\u22EF" menu ONE per
|
|
5824
|
+
// apply() call \u2014 the reflow loop keeps a stage active until it reports no
|
|
5825
|
+
// further progress, preserving the original per-button granularity.
|
|
5826
|
+
function foldStage(ids) {
|
|
5827
|
+
var folded = [];
|
|
5828
|
+
return {
|
|
5829
|
+
apply: function () {
|
|
5830
|
+
while (folded.length < ids.length) {
|
|
5831
|
+
var id = ids[folded.length];
|
|
5832
|
+
var el = movableFor(id);
|
|
5833
|
+
if (!el || el === moreDd || el.parentNode === moreMenu) { folded.push({ el: null, marker: null }); continue; }
|
|
5834
|
+
var m = markerFor(id, el);
|
|
5835
|
+
// A dropdown moved while open would strand its fixed-positioned menu.
|
|
5836
|
+
if (el.classList) el.classList.remove('open');
|
|
5837
|
+
moreDd.style.display = '';
|
|
5838
|
+
moreMenu.appendChild(el);
|
|
5839
|
+
folded.push({ el: el, marker: m });
|
|
5840
|
+
return true;
|
|
5841
|
+
}
|
|
5842
|
+
return false;
|
|
5843
|
+
},
|
|
5844
|
+
restore: function () {
|
|
5845
|
+
for (var i = folded.length - 1; i >= 0; i--) {
|
|
5846
|
+
var it = folded[i];
|
|
5847
|
+
if (it.el && it.marker && it.marker.parentNode) it.marker.parentNode.insertBefore(it.el, it.marker);
|
|
5848
|
+
}
|
|
5849
|
+
folded = [];
|
|
5850
|
+
},
|
|
5851
|
+
};
|
|
5852
|
+
}
|
|
5853
|
+
// Stage 3: the search input compacts behind a \u{1F50D} icon; the REAL input node
|
|
5854
|
+
// MOVES into the floating panel (fixed-positioned by the same helper as
|
|
5855
|
+
// every dropdown menu), so its input listener keeps driving state.query.
|
|
5856
|
+
function searchStage() {
|
|
5857
|
+
var on = false;
|
|
5858
|
+
return {
|
|
5859
|
+
apply: function () {
|
|
5860
|
+
if (on) return false;
|
|
5861
|
+
var inp = document.getElementById('search');
|
|
5862
|
+
var ddw = document.getElementById('searchDd');
|
|
5863
|
+
var menu = document.getElementById('searchMenu');
|
|
5864
|
+
if (!inp || !ddw || !menu) return false;
|
|
5865
|
+
menu.appendChild(inp);
|
|
5866
|
+
ddw.style.display = '';
|
|
5867
|
+
on = true;
|
|
5868
|
+
return true;
|
|
5869
|
+
},
|
|
5870
|
+
restore: function () {
|
|
5871
|
+
if (!on) return;
|
|
5872
|
+
on = false;
|
|
5873
|
+
var inp = document.getElementById('search');
|
|
5874
|
+
var ddw = document.getElementById('searchDd');
|
|
5875
|
+
if (inp && ddw && ddw.parentNode) ddw.parentNode.insertBefore(inp, ddw);
|
|
5876
|
+
if (ddw) { ddw.style.display = 'none'; if (ddw.classList) ddw.classList.remove('open'); }
|
|
5877
|
+
},
|
|
5878
|
+
};
|
|
5879
|
+
}
|
|
5880
|
+
// Stage 4: the mode tabs collapse into ONE dropdown labelled with the
|
|
5881
|
+
// current mode. The REAL tab buttons move into its menu (listeners and
|
|
5882
|
+
// active styling survive); updateHeaderSegs keeps the trigger label in
|
|
5883
|
+
// sync when the mode changes while compact.
|
|
5884
|
+
function modeStage() {
|
|
5885
|
+
var moved = [];
|
|
5886
|
+
return {
|
|
5887
|
+
apply: function () {
|
|
5888
|
+
if (moved.length) return false;
|
|
5889
|
+
var seg = document.getElementById('modeSeg');
|
|
5890
|
+
var ddw = document.getElementById('modeDd');
|
|
5891
|
+
var menu = document.getElementById('modeMenu');
|
|
5892
|
+
if (!seg || !ddw || !menu) return false;
|
|
5893
|
+
var btns = seg.querySelectorAll('button');
|
|
5894
|
+
if (!btns.length) return false;
|
|
5895
|
+
for (var i = 0; i < btns.length; i++) moved.push(btns[i]);
|
|
5896
|
+
for (var j = 0; j < moved.length; j++) menu.appendChild(moved[j]);
|
|
5897
|
+
seg.style.display = 'none';
|
|
5898
|
+
ddw.style.display = '';
|
|
5899
|
+
updateModeBtn();
|
|
5900
|
+
return true;
|
|
5901
|
+
},
|
|
5902
|
+
restore: function () {
|
|
5903
|
+
if (!moved.length) return;
|
|
5904
|
+
var seg = document.getElementById('modeSeg');
|
|
5905
|
+
var ddw = document.getElementById('modeDd');
|
|
5906
|
+
for (var i = 0; i < moved.length; i++) seg.appendChild(moved[i]);
|
|
5907
|
+
moved = [];
|
|
5908
|
+
seg.style.display = '';
|
|
5909
|
+
if (ddw) { ddw.style.display = 'none'; if (ddw.classList) ddw.classList.remove('open'); }
|
|
5910
|
+
},
|
|
5911
|
+
};
|
|
5912
|
+
}
|
|
5913
|
+
// Stage 5: crumb compaction is a render-mode toggle consulted by
|
|
5914
|
+
// renderCrumbs itself \u2014 see crumbsCompact there. Never reparenting.
|
|
5915
|
+
function crumbStage() {
|
|
5916
|
+
return {
|
|
5917
|
+
apply: function () {
|
|
5918
|
+
if (crumbsCompact) return false;
|
|
5919
|
+
crumbsCompact = true;
|
|
5920
|
+
renderCrumbs();
|
|
5921
|
+
return true;
|
|
5922
|
+
},
|
|
5923
|
+
restore: function () {
|
|
5924
|
+
if (!crumbsCompact) return;
|
|
5925
|
+
crumbsCompact = false;
|
|
5926
|
+
renderCrumbs();
|
|
5927
|
+
},
|
|
5928
|
+
};
|
|
5929
|
+
}
|
|
5930
|
+
// Ordered compaction stages: applied first-to-last only while the header
|
|
5931
|
+
// overflows, restored last-to-first when space returns.
|
|
5932
|
+
var STAGES = [
|
|
5933
|
+
foldStage(COLLAPSE), // 1: trailing buttons \u2192 "\u22EF" menu
|
|
5934
|
+
foldStage(['settingsBtn']), // 2: the View dropdown folds in too
|
|
5935
|
+
searchStage(), // 3: search input \u2192 \u{1F50D} + floating panel
|
|
5936
|
+
modeStage(), // 4: mode tabs \u2192 current-mode dropdown
|
|
5937
|
+
crumbStage(), // 5: ancestor crumbs \u2192 "\u2026" dropdown
|
|
5938
|
+
];
|
|
5279
5939
|
// Signed fit measure in px: positive = overflowing, negative = headroom.
|
|
5280
5940
|
// The header is a flex row whose ONLY flex:1 child is the .spacer, so the
|
|
5281
5941
|
// spacer's rendered width IS the free space -- it grows to absorb all slack
|
|
@@ -5291,33 +5951,54 @@ var MODEL = __MODEL_JSON__;
|
|
|
5291
5951
|
var slack = spacer ? spacer.getBoundingClientRect().width : 0;
|
|
5292
5952
|
return (hdr.scrollWidth - hdr.clientWidth) - slack;
|
|
5293
5953
|
}
|
|
5954
|
+
var inReflow = false;
|
|
5294
5955
|
function reflow() {
|
|
5295
5956
|
// Not laid out (hidden tab, non-browser DOM) \u2014 measuring would misfire.
|
|
5296
5957
|
var box = hdr.getBoundingClientRect();
|
|
5297
5958
|
if (!box || box.width <= 0) return;
|
|
5298
|
-
|
|
5299
|
-
|
|
5300
|
-
|
|
5301
|
-
|
|
5302
|
-
|
|
5303
|
-
|
|
5304
|
-
|
|
5305
|
-
|
|
5306
|
-
|
|
5307
|
-
|
|
5308
|
-
|
|
5309
|
-
|
|
5310
|
-
|
|
5311
|
-
|
|
5312
|
-
|
|
5313
|
-
var
|
|
5314
|
-
|
|
5315
|
-
|
|
5316
|
-
|
|
5317
|
-
|
|
5318
|
-
|
|
5319
|
-
|
|
5320
|
-
|
|
5959
|
+
inReflow = true;
|
|
5960
|
+
try {
|
|
5961
|
+
// The floating search panel must survive a reflow cycle: restore-all
|
|
5962
|
+
// would close it (and reparenting blurs the input), so capture its
|
|
5963
|
+
// open/focus state up front and reinstate it after the stage walk.
|
|
5964
|
+
var ddw = document.getElementById('searchDd');
|
|
5965
|
+
var inp = document.getElementById('search');
|
|
5966
|
+
var searchOpen = !!(ddw && String(ddw.className || '').indexOf('open') >= 0);
|
|
5967
|
+
var searchFocus = false;
|
|
5968
|
+
try {
|
|
5969
|
+
var ae = (typeof ROOT !== 'undefined' && ROOT ? ROOT : document).activeElement;
|
|
5970
|
+
searchFocus = !!(ae && inp && ae === inp);
|
|
5971
|
+
} catch (e) { /* stubbed DOM */ }
|
|
5972
|
+
// Restore every stage in REVERSE order, then re-apply progressively
|
|
5973
|
+
// while the row still overflows (idempotent).
|
|
5974
|
+
for (var i = STAGES.length - 1; i >= 0; i--) STAGES[i].restore();
|
|
5975
|
+
moreDd.style.display = 'none';
|
|
5976
|
+
hdr.scrollLeft = 0;
|
|
5977
|
+
// Demand a few px of headroom, not a bare fit \u2014 the marginal-fit widths
|
|
5978
|
+
// are exactly where the phantom scrollbar appeared. Every apply()
|
|
5979
|
+
// changes the very widths being measured, but restore-all + a strictly
|
|
5980
|
+
// forward stage walk make the outcome a pure function of the current
|
|
5981
|
+
// width, and reflow never reschedules itself (renderCrumbs' nudge is
|
|
5982
|
+
// suppressed via inReflow, and #hdr's own box never changes here), so
|
|
5983
|
+
// boundary widths settle in ONE pass instead of oscillating.
|
|
5984
|
+
var si = 0;
|
|
5985
|
+
var guard = 0;
|
|
5986
|
+
var bound = COLLAPSE.length + STAGES.length + 8;
|
|
5987
|
+
while (overflowPx() > -8 && si < STAGES.length && guard < bound) {
|
|
5988
|
+
guard++;
|
|
5989
|
+
if (!STAGES[si].apply()) si++;
|
|
5990
|
+
}
|
|
5991
|
+
if (searchOpen || searchFocus) {
|
|
5992
|
+
var compactNow = ddw && ddw.style && ddw.style.display !== 'none';
|
|
5993
|
+
if (compactNow && searchOpen) {
|
|
5994
|
+
if (ddw.classList) ddw.classList.add('open');
|
|
5995
|
+
positionDropdownMenu(ddw, document.getElementById('searchBtn'));
|
|
5996
|
+
}
|
|
5997
|
+
if (searchFocus && inp && inp.focus) inp.focus();
|
|
5998
|
+
}
|
|
5999
|
+
} finally {
|
|
6000
|
+
inReflow = false;
|
|
6001
|
+
}
|
|
5321
6002
|
}
|
|
5322
6003
|
var raf = null;
|
|
5323
6004
|
var defer = window.requestAnimationFrame
|
|
@@ -5327,6 +6008,9 @@ var MODEL = __MODEL_JSON__;
|
|
|
5327
6008
|
if (raf !== null) return;
|
|
5328
6009
|
raf = defer(function () { raf = null; reflow(); });
|
|
5329
6010
|
}
|
|
6011
|
+
// Crumb re-renders change the header's content width without resizing #hdr
|
|
6012
|
+
// itself \u2014 renderCrumbs nudges a reflow through this hook (no-op mid-reflow).
|
|
6013
|
+
headerReflowHook = function () { if (!inReflow) schedule(); };
|
|
5330
6014
|
if (typeof ResizeObserver !== 'undefined') {
|
|
5331
6015
|
new ResizeObserver(schedule).observe(hdr);
|
|
5332
6016
|
} else if (window.addEventListener) {
|
|
@@ -6654,7 +7338,7 @@ var init_extensions = __esm({
|
|
|
6654
7338
|
});
|
|
6655
7339
|
|
|
6656
7340
|
// src/core/rules/types.ts
|
|
6657
|
-
var BUILTIN_PROFILES;
|
|
7341
|
+
var BUILTIN_PROFILES, PROJECT_KINDS;
|
|
6658
7342
|
var init_types = __esm({
|
|
6659
7343
|
"src/core/rules/types.ts"() {
|
|
6660
7344
|
"use strict";
|
|
@@ -6667,6 +7351,7 @@ var init_types = __esm({
|
|
|
6667
7351
|
"realtime-embedded",
|
|
6668
7352
|
"plc-cyclic"
|
|
6669
7353
|
];
|
|
7354
|
+
PROJECT_KINDS = ["fullstack", "system-of-systems", "monorepo"];
|
|
6670
7355
|
}
|
|
6671
7356
|
});
|
|
6672
7357
|
|
|
@@ -6847,44 +7532,13 @@ var init_type_references = __esm({
|
|
|
6847
7532
|
}
|
|
6848
7533
|
});
|
|
6849
7534
|
|
|
6850
|
-
// src/
|
|
6851
|
-
function
|
|
6852
|
-
|
|
6853
|
-
system: loadSystemSpec(),
|
|
6854
|
-
subsystems: loadSubsystemSpecs(),
|
|
6855
|
-
components: loadComponentSpecs(),
|
|
6856
|
-
interfaces: loadInterfaceSpecs(),
|
|
6857
|
-
implementations: loadImplementationSpecs(),
|
|
6858
|
-
types: loadTypeSpecs()
|
|
6859
|
-
};
|
|
6860
|
-
const digest = crypto.createHash("sha256").update(canonicalize(tree)).digest("hex");
|
|
6861
|
-
return { algorithm: "sha256", digest };
|
|
6862
|
-
}
|
|
6863
|
-
function stateIdEquals(a, b) {
|
|
6864
|
-
return !!a && !!b && a.algorithm === b.algorithm && a.digest === b.digest;
|
|
6865
|
-
}
|
|
6866
|
-
function canonicalize(value) {
|
|
6867
|
-
return JSON.stringify(sortKeys(value));
|
|
7535
|
+
// src/utils/filenames.ts
|
|
7536
|
+
function safeFilenamePart(value) {
|
|
7537
|
+
return value.replace(/[^a-zA-Z0-9._-]/g, "-");
|
|
6868
7538
|
}
|
|
6869
|
-
|
|
6870
|
-
|
|
6871
|
-
if (v && typeof v === "object") {
|
|
6872
|
-
const src = v;
|
|
6873
|
-
const out = {};
|
|
6874
|
-
for (const k of Object.keys(src).sort()) {
|
|
6875
|
-
if (k === "createdAt" || k === "updatedAt") continue;
|
|
6876
|
-
out[k] = sortKeys(src[k]);
|
|
6877
|
-
}
|
|
6878
|
-
return out;
|
|
6879
|
-
}
|
|
6880
|
-
return v;
|
|
6881
|
-
}
|
|
6882
|
-
var crypto;
|
|
6883
|
-
var init_statehash = __esm({
|
|
6884
|
-
"src/core/statehash.ts"() {
|
|
7539
|
+
var init_filenames = __esm({
|
|
7540
|
+
"src/utils/filenames.ts"() {
|
|
6885
7541
|
"use strict";
|
|
6886
|
-
crypto = __toESM(require("crypto"));
|
|
6887
|
-
init_specs2();
|
|
6888
7542
|
}
|
|
6889
7543
|
});
|
|
6890
7544
|
|
|
@@ -7341,6 +7995,66 @@ function projectOwnSurface(maxAudience) {
|
|
|
7341
7995
|
function projectChildSurface() {
|
|
7342
7996
|
return projectOwnSurface("project");
|
|
7343
7997
|
}
|
|
7998
|
+
function localName(id) {
|
|
7999
|
+
return id.split("::").pop();
|
|
8000
|
+
}
|
|
8001
|
+
function projectSubsystemSurface(subsystemId) {
|
|
8002
|
+
const system = loadSystemSpec();
|
|
8003
|
+
if (!system) {
|
|
8004
|
+
throw new Error("Cannot project a subsystem surface: the L0 system spec is missing.");
|
|
8005
|
+
}
|
|
8006
|
+
const subsystems = loadSubsystemSpecs();
|
|
8007
|
+
const target = subsystems.find((s) => s.id === subsystemId);
|
|
8008
|
+
if (!target) {
|
|
8009
|
+
throw new Error(`Cannot project a subsystem surface: subsystem "${subsystemId}" does not exist.`);
|
|
8010
|
+
}
|
|
8011
|
+
const components = loadComponentSpecs();
|
|
8012
|
+
const interfaces = loadInterfaceSpecs();
|
|
8013
|
+
const types = loadTypeSpecs();
|
|
8014
|
+
const entries = [];
|
|
8015
|
+
const unprojectable = [];
|
|
8016
|
+
for (const pub of target.publicInterfaces ?? []) {
|
|
8017
|
+
if (!pub.component) continue;
|
|
8018
|
+
const comp = components.find((c) => c.id === pub.component || c.id === `${subsystemId}::${pub.component}`);
|
|
8019
|
+
if (!comp) continue;
|
|
8020
|
+
if (!CROSS_BOUNDARY_TARGETS.has(comp.componentType)) {
|
|
8021
|
+
unprojectable.push({ component: pub.component, componentType: comp.componentType });
|
|
8022
|
+
continue;
|
|
8023
|
+
}
|
|
8024
|
+
const compInterfaces = interfaces.filter((i) => i.component === comp.id && (!pub.interface || i.id === pub.interface || i.id === `${subsystemId}::${pub.interface}`));
|
|
8025
|
+
const methods = compInterfaces.flatMap((i) => i.methods);
|
|
8026
|
+
entries.push({
|
|
8027
|
+
id: localName(pub.interface ?? comp.id),
|
|
8028
|
+
name: comp.name,
|
|
8029
|
+
// Family ceiling: a sibling surface is consumable by the system family only.
|
|
8030
|
+
audience: "project",
|
|
8031
|
+
type: pub.type ?? "Custom",
|
|
8032
|
+
// The snapshot carries the LOCAL portal name — consumers resolve cross-tree
|
|
8033
|
+
// refs by their final segment.
|
|
8034
|
+
component: localName(comp.id),
|
|
8035
|
+
methods,
|
|
8036
|
+
...comp.dispatch && comp.dispatch.length ? { dispatch: comp.dispatch } : {},
|
|
8037
|
+
// Project the backing component's auth + basePath so the codec can emit
|
|
8038
|
+
// OpenAPI security + per-portal servers self-contained from the snapshot.
|
|
8039
|
+
...comp.auth && comp.auth.scheme !== "none" ? { auth: comp.auth } : {},
|
|
8040
|
+
...comp.basePath ? { basePath: comp.basePath } : {},
|
|
8041
|
+
details: pub.details ?? ""
|
|
8042
|
+
});
|
|
8043
|
+
}
|
|
8044
|
+
for (const skipped of unprojectable) {
|
|
8045
|
+
console.error(
|
|
8046
|
+
`[surfaces] skipped "${subsystemId}::${skipped.component}": a published ${skipped.componentType} can never serve a cross-boundary caller, so it stays out of every chained child's sibling surface \u2014 publish this surface through a Portal, a Gateway, or an Observer (for events).`
|
|
8047
|
+
);
|
|
8048
|
+
}
|
|
8049
|
+
return SurfaceSnapshotSchema.parse({
|
|
8050
|
+
projectName: `${system.name}::${subsystemId}`,
|
|
8051
|
+
origin: "generated",
|
|
8052
|
+
stateId: stateIdString(),
|
|
8053
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8054
|
+
interfaces: entries,
|
|
8055
|
+
types: computeTypeClosure(entries, types)
|
|
8056
|
+
});
|
|
8057
|
+
}
|
|
7344
8058
|
function listSnapshots(rootDir = getProjectRoot()) {
|
|
7345
8059
|
const dir = surfacesDir(rootDir);
|
|
7346
8060
|
if (!fs9.existsSync(dir)) return [];
|
|
@@ -7357,39 +8071,66 @@ function listSnapshots(rootDir = getProjectRoot()) {
|
|
|
7357
8071
|
function getSnapshot(projectName, rootDir = getProjectRoot()) {
|
|
7358
8072
|
return listSnapshots(rootDir).find((s) => s.projectName === projectName) ?? null;
|
|
7359
8073
|
}
|
|
8074
|
+
function snapshotFilename(projectName) {
|
|
8075
|
+
return `${safeFilenamePart(projectName)}.yaml`;
|
|
8076
|
+
}
|
|
7360
8077
|
function saveSnapshot(snapshot, rootDir = getProjectRoot()) {
|
|
7361
8078
|
const dir = surfacesDir(rootDir);
|
|
7362
8079
|
fs9.mkdirSync(dir, { recursive: true });
|
|
7363
|
-
const p = path10.join(dir,
|
|
8080
|
+
const p = path10.join(dir, snapshotFilename(snapshot.projectName));
|
|
7364
8081
|
writeYamlFile(p, SurfaceSnapshotSchema.parse(snapshot));
|
|
7365
8082
|
return p;
|
|
7366
8083
|
}
|
|
7367
8084
|
function loadSurfaceSnapshots() {
|
|
7368
8085
|
return listSnapshots();
|
|
7369
8086
|
}
|
|
7370
|
-
function
|
|
7371
|
-
const
|
|
7372
|
-
|
|
8087
|
+
function selectPortalSpec(renderedSet, portalId) {
|
|
8088
|
+
const hit = renderedSet.find((spec) => spec.portalId === portalId);
|
|
8089
|
+
if (!hit) {
|
|
8090
|
+
const known = renderedSet.map((s) => s.portalId).join(", ");
|
|
8091
|
+
throw new Error(`Unknown portal "${portalId}" \u2014 this surface renders: ${known || "(no portals)"}.`);
|
|
8092
|
+
}
|
|
8093
|
+
return [hit];
|
|
8094
|
+
}
|
|
8095
|
+
function perPortalPath(resolvedOut, portalId) {
|
|
8096
|
+
const ext = path10.extname(resolvedOut);
|
|
8097
|
+
const stem2 = ext ? resolvedOut.slice(0, -ext.length) : resolvedOut;
|
|
8098
|
+
return `${stem2}.${safeFilenamePart(portalId)}${ext}`;
|
|
7373
8099
|
}
|
|
7374
|
-
function writeSurfaceFile(outPath,
|
|
8100
|
+
function writeSurfaceFile(outPath, snapshot, renderedSet) {
|
|
7375
8101
|
const resolved = path10.resolve(outPath);
|
|
7376
8102
|
fs9.mkdirSync(path10.dirname(resolved), { recursive: true });
|
|
7377
|
-
if (
|
|
7378
|
-
|
|
7379
|
-
|
|
8103
|
+
if (!renderedSet || renderedSet.length === 0) {
|
|
8104
|
+
writeYamlFile(resolved, snapshot);
|
|
8105
|
+
return [resolved];
|
|
8106
|
+
}
|
|
8107
|
+
if (renderedSet.length === 1) {
|
|
8108
|
+
fs9.writeFileSync(resolved, renderedSet[0].document);
|
|
8109
|
+
return [resolved];
|
|
8110
|
+
}
|
|
8111
|
+
return renderedSet.map((spec) => {
|
|
8112
|
+
const target = perPortalPath(resolved, spec.portalId);
|
|
8113
|
+
fs9.writeFileSync(target, spec.document);
|
|
8114
|
+
return target;
|
|
8115
|
+
});
|
|
7380
8116
|
}
|
|
7381
|
-
function
|
|
7382
|
-
const
|
|
7383
|
-
const openapi = format === "openapi" ? renderOpenApiForms(snapshot) : void 0;
|
|
7384
|
-
const body = openapi?.rendered ?? openapi?.renderedSet[0]?.document;
|
|
7385
|
-
const writtenTo = outPath ? writeSurfaceFile(outPath, body, snapshot) : void 0;
|
|
8117
|
+
function exportResult(snapshot, renderedSet, writtenPaths) {
|
|
8118
|
+
const rendered = renderedSet?.length === 1 ? renderedSet[0].document : void 0;
|
|
7386
8119
|
return {
|
|
7387
8120
|
snapshot,
|
|
7388
|
-
...
|
|
7389
|
-
...
|
|
7390
|
-
...
|
|
8121
|
+
...rendered !== void 0 ? { rendered } : {},
|
|
8122
|
+
...renderedSet ? { renderedSet } : {},
|
|
8123
|
+
...writtenPaths.length === 1 ? { writtenTo: writtenPaths[0] } : {},
|
|
8124
|
+
...writtenPaths.length ? { writtenPaths } : {}
|
|
7391
8125
|
};
|
|
7392
8126
|
}
|
|
8127
|
+
function exportSurface(maxAudience, format, outPath, portalId) {
|
|
8128
|
+
const snapshot = projectOwnSurface(maxAudience);
|
|
8129
|
+
let renderedSet = format === "openapi" ? toOpenApiSet(snapshot) : void 0;
|
|
8130
|
+
if (renderedSet && portalId) renderedSet = selectPortalSpec(renderedSet, portalId);
|
|
8131
|
+
const writtenPaths = outPath ? writeSurfaceFile(outPath, snapshot, renderedSet) : [];
|
|
8132
|
+
return exportResult(snapshot, renderedSet, writtenPaths);
|
|
8133
|
+
}
|
|
7393
8134
|
function importSurface(sourcePath, origin) {
|
|
7394
8135
|
const resolved = path10.resolve(sourcePath);
|
|
7395
8136
|
if (!fs9.existsSync(resolved)) {
|
|
@@ -7409,17 +8150,54 @@ function importSurface(sourcePath, origin) {
|
|
|
7409
8150
|
return snapshot;
|
|
7410
8151
|
}
|
|
7411
8152
|
function generateChildSnapshots(rootDir = getProjectRoot()) {
|
|
7412
|
-
const
|
|
8153
|
+
const topLevel = loadSubsystemSpecs().filter((s) => !s.id.includes("::"));
|
|
8154
|
+
const children = topLevel.filter((s) => s.projectPath);
|
|
7413
8155
|
if (!children.length) return [];
|
|
7414
|
-
const
|
|
8156
|
+
const familySnapshot = projectChildSurface();
|
|
8157
|
+
const siblingSnapshots = /* @__PURE__ */ new Map();
|
|
8158
|
+
const siblingSurface = (subsystemId) => {
|
|
8159
|
+
let snap = siblingSnapshots.get(subsystemId);
|
|
8160
|
+
if (!snap) {
|
|
8161
|
+
snap = projectSubsystemSurface(subsystemId);
|
|
8162
|
+
siblingSnapshots.set(subsystemId, snap);
|
|
8163
|
+
}
|
|
8164
|
+
return snap;
|
|
8165
|
+
};
|
|
7415
8166
|
const written = [];
|
|
7416
8167
|
for (const child of children) {
|
|
7417
8168
|
const childDir = path10.resolve(rootDir, child.projectPath);
|
|
7418
8169
|
if (!fs9.existsSync(childDir)) continue;
|
|
7419
|
-
written.push(saveSnapshot(
|
|
8170
|
+
written.push(saveSnapshot(familySnapshot, childDir));
|
|
8171
|
+
for (const sibling of topLevel) {
|
|
8172
|
+
if (sibling.id === child.id) continue;
|
|
8173
|
+
written.push(saveSnapshot(siblingSurface(sibling.id), childDir));
|
|
8174
|
+
}
|
|
7420
8175
|
}
|
|
7421
8176
|
return written;
|
|
7422
8177
|
}
|
|
8178
|
+
function computeParentStateId(parentRoot) {
|
|
8179
|
+
return computeStateIdAt(parentRoot);
|
|
8180
|
+
}
|
|
8181
|
+
function listExternalInterfaces() {
|
|
8182
|
+
const snapshots = listSnapshots();
|
|
8183
|
+
const chainingParent = resolveChainingParent();
|
|
8184
|
+
const parentStateId = chainingParent ? computeParentStateId(chainingParent.parentRoot) : null;
|
|
8185
|
+
return snapshots.map((snapshot) => {
|
|
8186
|
+
const generated = snapshot.origin === "generated";
|
|
8187
|
+
const sourceKind = !generated ? "foreign" : snapshot.projectName.includes("::") ? "sibling" : "parent";
|
|
8188
|
+
const freshness = generated && parentStateId ? snapshot.stateId === parentStateId ? "fresh" : "stale" : "unverifiable";
|
|
8189
|
+
return {
|
|
8190
|
+
projectName: snapshot.projectName,
|
|
8191
|
+
origin: snapshot.origin,
|
|
8192
|
+
sourceKind,
|
|
8193
|
+
generatedAt: snapshot.generatedAt,
|
|
8194
|
+
...snapshot.stateId ? { stateId: snapshot.stateId } : {},
|
|
8195
|
+
...snapshot.version ? { version: snapshot.version } : {},
|
|
8196
|
+
freshness,
|
|
8197
|
+
interfaceIds: snapshot.interfaces.map((e) => e.id)
|
|
8198
|
+
};
|
|
8199
|
+
});
|
|
8200
|
+
}
|
|
7423
8201
|
function surfaceContentKey(snapshot) {
|
|
7424
8202
|
const { stateId, generatedAt, origin, ...content } = snapshot;
|
|
7425
8203
|
return JSON.stringify(content);
|
|
@@ -7446,7 +8224,7 @@ function checkChildSurfaceFreshness(rootDir = getProjectRoot()) {
|
|
|
7446
8224
|
}
|
|
7447
8225
|
return issues;
|
|
7448
8226
|
}
|
|
7449
|
-
var fs9, path10, SURFACES_DIRNAME;
|
|
8227
|
+
var fs9, path10, SURFACES_DIRNAME, CROSS_BOUNDARY_TARGETS;
|
|
7450
8228
|
var init_surfaces = __esm({
|
|
7451
8229
|
"src/core/surfaces.ts"() {
|
|
7452
8230
|
"use strict";
|
|
@@ -7454,12 +8232,14 @@ var init_surfaces = __esm({
|
|
|
7454
8232
|
path10 = __toESM(require("path"));
|
|
7455
8233
|
init_fs();
|
|
7456
8234
|
init_yaml();
|
|
8235
|
+
init_filenames();
|
|
7457
8236
|
init_models();
|
|
7458
8237
|
init_specs2();
|
|
7459
8238
|
init_statehash();
|
|
7460
8239
|
init_type_analysis();
|
|
7461
8240
|
init_openapi();
|
|
7462
8241
|
SURFACES_DIRNAME = "surfaces";
|
|
8242
|
+
CROSS_BOUNDARY_TARGETS = /* @__PURE__ */ new Set(["Portal", "Gateway", "Observer"]);
|
|
7463
8243
|
}
|
|
7464
8244
|
});
|
|
7465
8245
|
|
|
@@ -7627,7 +8407,8 @@ var init_contracts = __esm({
|
|
|
7627
8407
|
"SURFACE_REF_NOT_EXPOSED",
|
|
7628
8408
|
`Method "${implMethod.name}" in implementation "${impl.id}" dispatches capability "${step.capability}" through cross-tree portal "${step.targetComponent}" (step ${step.stepNumber}), but the surface snapshot of "${resolved.snapshot.projectName}" does not serve that capability on "${resolved.entry.id}".`,
|
|
7629
8409
|
impl.id,
|
|
7630
|
-
isDraftCtx
|
|
8410
|
+
isDraftCtx,
|
|
8411
|
+
true
|
|
7631
8412
|
);
|
|
7632
8413
|
}
|
|
7633
8414
|
continue;
|
|
@@ -7684,7 +8465,8 @@ var init_contracts = __esm({
|
|
|
7684
8465
|
"SURFACE_REF_NOT_EXPOSED",
|
|
7685
8466
|
`Method "${implMethod.name}" in implementation "${impl.id}" calls "${step.targetMethod}" on cross-tree component "${step.targetComponent}" (step ${step.stepNumber}), but the surface snapshot of "${resolved.snapshot.projectName}" does not expose that method on "${resolved.entry.id}".`,
|
|
7686
8467
|
impl.id,
|
|
7687
|
-
isDraftCtx
|
|
8468
|
+
isDraftCtx,
|
|
8469
|
+
true
|
|
7688
8470
|
);
|
|
7689
8471
|
} else if (step.assertsGuarantees) {
|
|
7690
8472
|
const declared = new Set(surfaceMethod.guarantees ?? []);
|
|
@@ -7695,7 +8477,8 @@ var init_contracts = __esm({
|
|
|
7695
8477
|
"NARRATIVE_SEMANTIC_UNBACKED",
|
|
7696
8478
|
`Step ${step.stepNumber} of "${implMethod.name}" in implementation "${impl.id}" asserts guarantee "${g}", but the surface snapshot of "${resolved.snapshot.projectName}" does not declare it on "${resolved.entry.id}.${step.targetMethod}".`,
|
|
7697
8479
|
impl.id,
|
|
7698
|
-
isDraftCtx
|
|
8480
|
+
isDraftCtx,
|
|
8481
|
+
true
|
|
7699
8482
|
);
|
|
7700
8483
|
}
|
|
7701
8484
|
}
|
|
@@ -9046,13 +9829,14 @@ var init_portals = __esm({
|
|
|
9046
9829
|
};
|
|
9047
9830
|
portalsRule = {
|
|
9048
9831
|
name: "portal-endpoints",
|
|
9049
|
-
description: "A Portal declares its portalType and binds every interface method to a concrete endpoint of the matching transport. Non-Portal components carry no portalType, basePath, or
|
|
9832
|
+
description: "A Portal declares its portalType and binds every interface method to a concrete endpoint of the matching transport. Non-Portal components carry no portalType, basePath, endpoints, or auth (auth is inbound transport auth \u2014 a Gateway carries it on the Portal it owns).",
|
|
9050
9833
|
codes: [
|
|
9051
9834
|
{ code: "MISSING_PORTAL_TYPE", defaultSeverity: "error", summary: "Portal without a portalType" },
|
|
9052
9835
|
{ code: "MISSING_ENDPOINT", defaultSeverity: "error", summary: "Portal method without a wire endpoint binding" },
|
|
9053
9836
|
{ code: "ENDPOINT_TRANSPORT_MISMATCH", defaultSeverity: "error", summary: "Endpoint transport does not match the Portal portalType" },
|
|
9054
9837
|
{ code: "UNEXPECTED_PORTAL_FIELD", defaultSeverity: "error", summary: "Non-Portal component with portalType/basePath" },
|
|
9055
|
-
{ code: "ARCHITECTURE_VIOLATION_NON_PORTAL_ENDPOINT", defaultSeverity: "error", summary: "Non-Portal component method declaring an endpoint" }
|
|
9838
|
+
{ code: "ARCHITECTURE_VIOLATION_NON_PORTAL_ENDPOINT", defaultSeverity: "error", summary: "Non-Portal component method declaring an endpoint" },
|
|
9839
|
+
{ code: "AUTH_ON_NON_PORTAL", defaultSeverity: "warning", summary: "Non-Portal component declaring auth (auth is inbound transport auth, only meaningful on a Portal)" }
|
|
9056
9840
|
],
|
|
9057
9841
|
check(ctx) {
|
|
9058
9842
|
for (const comp of ctx.components) {
|
|
@@ -9104,6 +9888,15 @@ var init_portals = __esm({
|
|
|
9104
9888
|
isDraftCtx
|
|
9105
9889
|
);
|
|
9106
9890
|
}
|
|
9891
|
+
if (comp.auth !== void 0) {
|
|
9892
|
+
ctx.addIssue(
|
|
9893
|
+
"warning",
|
|
9894
|
+
"AUTH_ON_NON_PORTAL",
|
|
9895
|
+
`Component "${comp.id}" is a ${comp.componentType}, not a Portal, but declares "auth". Auth is inbound transport auth and is only meaningful on a Portal (a Gateway carries it on the Portal it owns). Move it to the exposed Portal, or remove it.`,
|
|
9896
|
+
comp.id,
|
|
9897
|
+
isDraftCtx
|
|
9898
|
+
);
|
|
9899
|
+
}
|
|
9107
9900
|
const compInterfaces = ctx.interfaces.filter((i) => i.component === comp.id);
|
|
9108
9901
|
for (const intf of compInterfaces) {
|
|
9109
9902
|
for (const m of intf.methods) {
|
|
@@ -9173,7 +9966,8 @@ var init_stereotype_deps = __esm({
|
|
|
9173
9966
|
"CROSS_SUBSYSTEM_NON_ADAPTER",
|
|
9174
9967
|
`Boundary violation: ${comp.componentType} "${comp.id}" depends directly on "${depId}", a surface of project "${resolved.snapshot.projectName}". Only a local client Adapter may cross a project boundary \u2014 route this hop through an Adapter.`,
|
|
9175
9968
|
comp.id,
|
|
9176
|
-
isDraftCtx
|
|
9969
|
+
isDraftCtx,
|
|
9970
|
+
true
|
|
9177
9971
|
);
|
|
9178
9972
|
}
|
|
9179
9973
|
continue;
|
|
@@ -9751,14 +10545,14 @@ var init_declarative_assertions = __esm({
|
|
|
9751
10545
|
});
|
|
9752
10546
|
|
|
9753
10547
|
// src/core/rules/profiles.ts
|
|
9754
|
-
var BACKEND_LIKE, FRONTEND_LIKE,
|
|
10548
|
+
var BACKEND_LIKE, FRONTEND_LIKE, PROJECT_KINDS2, profilesRule;
|
|
9755
10549
|
var init_profiles = __esm({
|
|
9756
10550
|
"src/core/rules/profiles.ts"() {
|
|
9757
10551
|
"use strict";
|
|
9758
10552
|
init_types();
|
|
9759
10553
|
BACKEND_LIKE = /* @__PURE__ */ new Set(["backend", "lowlevel-os", "game-ecs", "realtime-embedded", "plc-cyclic"]);
|
|
9760
10554
|
FRONTEND_LIKE = /* @__PURE__ */ new Set(["frontend-reactive", "frontend-controller"]);
|
|
9761
|
-
|
|
10555
|
+
PROJECT_KINDS2 = new Set(PROJECT_KINDS);
|
|
9762
10556
|
profilesRule = {
|
|
9763
10557
|
name: "architectural-profiles",
|
|
9764
10558
|
description: "Per-profile stereotype constraints: View/FeatureComponent/RouterComponent only in frontend profiles; Actor/Supervisor forbidden in plc-cyclic (single scan cycle); Actor/Supervisor in frontend profiles warned. Extension packs may register custom profiles (family + forbidden/discouraged stereotype lists); unknown profile names are flagged.",
|
|
@@ -9782,7 +10576,7 @@ var init_profiles = __esm({
|
|
|
9782
10576
|
);
|
|
9783
10577
|
}
|
|
9784
10578
|
}
|
|
9785
|
-
if (!registered.has(ctx.projectType) && !
|
|
10579
|
+
if (!registered.has(ctx.projectType) && !PROJECT_KINDS2.has(ctx.projectType)) {
|
|
9786
10580
|
ctx.addIssue(
|
|
9787
10581
|
"warning",
|
|
9788
10582
|
"UNKNOWN_PROFILE",
|
|
@@ -11036,11 +11830,11 @@ var init_narrative_antipatterns = __esm({
|
|
|
11036
11830
|
const memberEdges = keys.flatMap((k) => (adjacency.get(k) ?? []).filter((e) => inScc.has(e.toKey)));
|
|
11037
11831
|
if (memberEdges.length === 0) continue;
|
|
11038
11832
|
const anchor = [...memberEdges].sort((a, b) => a.fromKey.localeCompare(b.fromKey))[0];
|
|
11039
|
-
const
|
|
11833
|
+
const path61 = [...keys].sort().join(" \u2192 ");
|
|
11040
11834
|
ctx.addIssue(
|
|
11041
11835
|
"warning",
|
|
11042
11836
|
"UNCONDITIONAL_CALL_CYCLE",
|
|
11043
|
-
`Call cycle with no guard: ${
|
|
11837
|
+
`Call cycle with no guard: ${path61} \u2014 every call edge in this cycle is unavoidable on all paths of its narrative (e.g. step ${anchor.stepNumber} of "${anchor.methodName}" in "${anchor.impl.id}" always calls ${anchor.toLabel}). This recurses without a base case, by construction. Guard at least one edge with a branch/return before the call, or lint.allow with the termination argument.`,
|
|
11044
11838
|
anchor.impl.id,
|
|
11045
11839
|
memberEdges.some((e) => ctx.isImplementationDraft(e.impl))
|
|
11046
11840
|
);
|
|
@@ -12187,6 +12981,95 @@ var init_lint_allows = __esm({
|
|
|
12187
12981
|
}
|
|
12188
12982
|
});
|
|
12189
12983
|
|
|
12984
|
+
// src/core/rules/portal-call-auth.ts
|
|
12985
|
+
var COMPONENT_REF_PREFIX, portalCallAuthRule;
|
|
12986
|
+
var init_portal_call_auth = __esm({
|
|
12987
|
+
"src/core/rules/portal-call-auth.ts"() {
|
|
12988
|
+
"use strict";
|
|
12989
|
+
COMPONENT_REF_PREFIX = "component:";
|
|
12990
|
+
portalCallAuthRule = {
|
|
12991
|
+
name: "portal-call-auth",
|
|
12992
|
+
description: "Hardens authenticated cross-service calls. An OUTBOUND narrative `call` into ANOTHER component's Portal whose auth is not `none` must (a) be made by an Adapter \u2014 the only block that does external I/O \u2014 and (b) declare the credential source it presents via the step's `auth.from`. Absence of a source warns PORTAL_AUTH_UNMET; a non-Adapter presenter warns AUTH_PRESENTER_NOT_ADAPTER. When `auth.from` is a modeled reference (`component:<id>`) it must resolve to an Adapter/Store the presenter is wired to (UNKNOWN_AUTH_SOURCE / AUTH_SOURCE_NOT_PROVIDER / AUTH_SOURCE_UNWIRED). The actual secret is never stored in the spec. Dispatch steps (a portal's OWN inbound routing) and self-calls are not cross-service calls and are excluded.",
|
|
12993
|
+
codes: [
|
|
12994
|
+
{ code: "PORTAL_AUTH_UNMET", defaultSeverity: "warning", summary: "A narrative call into another component's authed Portal does not declare where its credential loads from" },
|
|
12995
|
+
{ code: "AUTH_PRESENTER_NOT_ADAPTER", defaultSeverity: "warning", summary: "A non-Adapter component authenticates an outbound call to a portal (external I/O must go through an Adapter)" },
|
|
12996
|
+
{ code: "UNKNOWN_AUTH_SOURCE", defaultSeverity: "warning", summary: "auth.from references a component: source that does not exist" },
|
|
12997
|
+
{ code: "AUTH_SOURCE_NOT_PROVIDER", defaultSeverity: "warning", summary: "auth.from references a component that is not an Adapter or Store" },
|
|
12998
|
+
{ code: "AUTH_SOURCE_UNWIRED", defaultSeverity: "warning", summary: "The presenter declares a component: credential source it does not depend on or own" }
|
|
12999
|
+
],
|
|
13000
|
+
check(ctx) {
|
|
13001
|
+
for (const impl of ctx.implementations) {
|
|
13002
|
+
const ownComponent = ctx.interfaceMap.get(impl.contract)?.component;
|
|
13003
|
+
const presenter = ownComponent ? ctx.componentMap.get(ownComponent) : void 0;
|
|
13004
|
+
const draft = ctx.isImplementationDraft(impl);
|
|
13005
|
+
for (const method2 of impl.methods ?? []) {
|
|
13006
|
+
for (const step of method2.narrative ?? []) {
|
|
13007
|
+
if (step.type !== "call") continue;
|
|
13008
|
+
if (!step.targetComponent || step.targetComponent === ownComponent) continue;
|
|
13009
|
+
const target = ctx.componentMap.get(step.targetComponent);
|
|
13010
|
+
if (!target || target.componentType !== "Portal") continue;
|
|
13011
|
+
if (!target.auth || target.auth.scheme === "none") continue;
|
|
13012
|
+
if (presenter && presenter.componentType !== "Adapter") {
|
|
13013
|
+
ctx.addIssue(
|
|
13014
|
+
"warning",
|
|
13015
|
+
"AUTH_PRESENTER_NOT_ADAPTER",
|
|
13016
|
+
`Narrative step ${step.stepNumber} of "${method2.name}" in "${impl.id}": "${presenter.id}" (${presenter.componentType}) authenticates an outbound call to portal "${target.id}". An authenticated cross-service call is external I/O and must be made by an Adapter (the only block that does external I/O) \u2014 route it through a client Adapter.`,
|
|
13017
|
+
impl.id,
|
|
13018
|
+
draft
|
|
13019
|
+
);
|
|
13020
|
+
}
|
|
13021
|
+
const from = step.auth?.from;
|
|
13022
|
+
if (!from) {
|
|
13023
|
+
ctx.addIssue(
|
|
13024
|
+
"warning",
|
|
13025
|
+
"PORTAL_AUTH_UNMET",
|
|
13026
|
+
`Narrative step ${step.stepNumber} of "${method2.name}" in "${impl.id}" calls the authenticated portal "${target.id}" (auth scheme: ${target.auth.scheme}) but does not declare where its credential is loaded from. Add the credential source (the step's auth.from \u2014 a secret-store component, env var, config key, or vault ref), or drop the portal's auth if it needs none.`,
|
|
13027
|
+
impl.id,
|
|
13028
|
+
draft
|
|
13029
|
+
);
|
|
13030
|
+
continue;
|
|
13031
|
+
}
|
|
13032
|
+
if (from.startsWith(COMPONENT_REF_PREFIX)) {
|
|
13033
|
+
const srcId = from.slice(COMPONENT_REF_PREFIX.length);
|
|
13034
|
+
const src = ctx.componentMap.get(srcId);
|
|
13035
|
+
if (!src) {
|
|
13036
|
+
ctx.addIssue(
|
|
13037
|
+
"warning",
|
|
13038
|
+
"UNKNOWN_AUTH_SOURCE",
|
|
13039
|
+
`Narrative step ${step.stepNumber} of "${method2.name}" in "${impl.id}": auth.from references component "${srcId}", which does not exist.`,
|
|
13040
|
+
impl.id,
|
|
13041
|
+
draft
|
|
13042
|
+
);
|
|
13043
|
+
} else {
|
|
13044
|
+
if (src.componentType !== "Adapter" && src.componentType !== "Store") {
|
|
13045
|
+
ctx.addIssue(
|
|
13046
|
+
"warning",
|
|
13047
|
+
"AUTH_SOURCE_NOT_PROVIDER",
|
|
13048
|
+
`Narrative step ${step.stepNumber} of "${method2.name}" in "${impl.id}": auth.from references "${srcId}" (${src.componentType}); a credential source must be an Adapter (loads the secret via external I/O) or a Store (holds it).`,
|
|
13049
|
+
impl.id,
|
|
13050
|
+
draft
|
|
13051
|
+
);
|
|
13052
|
+
}
|
|
13053
|
+
const wired = (presenter?.dependsOn ?? []).includes(srcId) || (presenter?.owns ?? []).includes(srcId);
|
|
13054
|
+
if (presenter && !wired) {
|
|
13055
|
+
ctx.addIssue(
|
|
13056
|
+
"warning",
|
|
13057
|
+
"AUTH_SOURCE_UNWIRED",
|
|
13058
|
+
`Narrative step ${step.stepNumber} of "${method2.name}" in "${impl.id}": "${presenter.id}" loads its credential from "${srcId}" but neither depends on nor owns it \u2014 declare the dependsOn edge so the credential wiring is real.`,
|
|
13059
|
+
impl.id,
|
|
13060
|
+
draft
|
|
13061
|
+
);
|
|
13062
|
+
}
|
|
13063
|
+
}
|
|
13064
|
+
}
|
|
13065
|
+
}
|
|
13066
|
+
}
|
|
13067
|
+
}
|
|
13068
|
+
}
|
|
13069
|
+
};
|
|
13070
|
+
}
|
|
13071
|
+
});
|
|
13072
|
+
|
|
12190
13073
|
// src/core/rules/index.ts
|
|
12191
13074
|
function makeScopeFilter(opts) {
|
|
12192
13075
|
const { components, interfaces, implementations, types, scopeSubsystem } = opts;
|
|
@@ -12338,9 +13221,15 @@ function buildRuleContext(opts) {
|
|
|
12338
13221
|
...[...SDD_RULES, ...extensions.rules].flatMap((r) => r.codes.map((c) => c.code)),
|
|
12339
13222
|
// Declarative assertions bring their own namespaced codes — lint.allow
|
|
12340
13223
|
// and severity overrides treat them exactly like builtins.
|
|
12341
|
-
...extensions.assertions.map((a) => a.fullCode)
|
|
13224
|
+
...extensions.assertions.map((a) => a.fullCode),
|
|
13225
|
+
// Entry-point emitted codes: validateSddTree's chained-subproject pass
|
|
13226
|
+
// raises these AFTER the rule run (it post-processes the aggregated issue
|
|
13227
|
+
// list), so no registered rule declares them — but lint.allow validation
|
|
13228
|
+
// must still recognize them as real codes.
|
|
13229
|
+
"CHAINED_SUBPROJECT_CONTEXT",
|
|
13230
|
+
"UNVERIFIED_EXTERNAL_REF"
|
|
12342
13231
|
]);
|
|
12343
|
-
const addIssue = (defaultSeverity, code, message, specId, isDraftContext) => {
|
|
13232
|
+
const addIssue = (defaultSeverity, code, message, specId, isDraftContext, surfaceResolved) => {
|
|
12344
13233
|
if (scopeSubsystem && specId && !isSpecInScope(specId)) {
|
|
12345
13234
|
return;
|
|
12346
13235
|
}
|
|
@@ -12358,7 +13247,14 @@ function buildRuleContext(opts) {
|
|
|
12358
13247
|
if (severity === "warning") return;
|
|
12359
13248
|
}
|
|
12360
13249
|
}
|
|
12361
|
-
issues.push({
|
|
13250
|
+
issues.push({
|
|
13251
|
+
severity,
|
|
13252
|
+
code,
|
|
13253
|
+
message,
|
|
13254
|
+
specId,
|
|
13255
|
+
...isDraftContext ? { draftContext: true } : {},
|
|
13256
|
+
...surfaceResolved ? { surfaceResolved: true } : {}
|
|
13257
|
+
});
|
|
12362
13258
|
};
|
|
12363
13259
|
return {
|
|
12364
13260
|
system,
|
|
@@ -12432,6 +13328,7 @@ var init_rules = __esm({
|
|
|
12432
13328
|
init_hidden_state();
|
|
12433
13329
|
init_dependency_conformance();
|
|
12434
13330
|
init_lint_allows();
|
|
13331
|
+
init_portal_call_auth();
|
|
12435
13332
|
init_source_analysis();
|
|
12436
13333
|
init_types();
|
|
12437
13334
|
init_type_analysis();
|
|
@@ -12453,6 +13350,9 @@ var init_rules = __esm({
|
|
|
12453
13350
|
narrativeAntipatternsRule,
|
|
12454
13351
|
narrativeDetailRule,
|
|
12455
13352
|
portalsRule,
|
|
13353
|
+
// Cross-call auth: a narrative call into an authed Portal must name its
|
|
13354
|
+
// credential source (rides with the portal family).
|
|
13355
|
+
portalCallAuthRule,
|
|
12456
13356
|
stereotypeDepsRule,
|
|
12457
13357
|
patternsRule,
|
|
12458
13358
|
// Facade shape rides with pattern ownership: same §7 doctrine, narrative side.
|
|
@@ -12515,6 +13415,7 @@ var init_rules = __esm({
|
|
|
12515
13415
|
// L4 expectations: implementations and their code linkage.
|
|
12516
13416
|
MISSING_IMPLEMENTATION_METHOD: "implementations",
|
|
12517
13417
|
MISSING_SOURCE_PATH: "implementations",
|
|
13418
|
+
PORTAL_AUTH_UNMET: "implementations",
|
|
12518
13419
|
MISSING_SOURCE_FILE: "implementations",
|
|
12519
13420
|
SOURCE_PATH_ESCAPES_ROOT: "implementations",
|
|
12520
13421
|
UNREALIZED_METHOD: "implementations",
|
|
@@ -12778,24 +13679,54 @@ function validateSddTree(rulesOrOptions, projectType = "backend") {
|
|
|
12778
13679
|
for (const rule of ruleSequence()) {
|
|
12779
13680
|
rule.check(ctx);
|
|
12780
13681
|
}
|
|
12781
|
-
const hasCrossTreeSuspects = issues.some(
|
|
13682
|
+
const hasCrossTreeSuspects = issues.some(
|
|
13683
|
+
(i) => SUBPROJECT_REFERENCE_CODES.has(i.code) || SUBPROJECT_CONFORMANCE_CODES.has(i.code)
|
|
13684
|
+
);
|
|
12782
13685
|
const chainingParent = hasCrossTreeSuspects ? findChainingParent(getProjectRoot()) : null;
|
|
12783
13686
|
if (chainingParent) {
|
|
13687
|
+
let unverified = 0;
|
|
12784
13688
|
let downgraded = 0;
|
|
12785
|
-
for (
|
|
12786
|
-
|
|
12787
|
-
if (iss.
|
|
12788
|
-
|
|
12789
|
-
|
|
13689
|
+
for (let at = 0; at < issues.length; at++) {
|
|
13690
|
+
const iss = issues[at];
|
|
13691
|
+
if (SUBPROJECT_REFERENCE_CODES.has(iss.code) && !iss.surfaceResolved) {
|
|
13692
|
+
issues[at] = {
|
|
13693
|
+
severity: "warning",
|
|
13694
|
+
code: "UNVERIFIED_EXTERNAL_REF",
|
|
13695
|
+
crossTreeContext: true,
|
|
13696
|
+
// --ci waives it (parent root is authoritative)
|
|
13697
|
+
specId: iss.specId,
|
|
13698
|
+
...iss.agentId ? { agentId: iss.agentId } : {},
|
|
13699
|
+
...iss.draftContext ? { draftContext: true } : {},
|
|
13700
|
+
message: `Unverified external reference (${iss.code}): ${iss.message} No vendored surface snapshot covers this reference, so it cannot be verified from this chained subproject standalone \u2014 re-lock the parent so fresh family/sibling snapshots ship, or inspect what this project can consume via \`wairon surface externals\` / sdd_list_external_interfaces.`
|
|
13701
|
+
};
|
|
13702
|
+
unverified++;
|
|
13703
|
+
continue;
|
|
13704
|
+
}
|
|
13705
|
+
if (SUBPROJECT_CONFORMANCE_CODES.has(iss.code)) {
|
|
13706
|
+
if (iss.severity === "error") {
|
|
13707
|
+
iss.severity = "warning";
|
|
13708
|
+
downgraded++;
|
|
13709
|
+
}
|
|
13710
|
+
iss.crossTreeContext = true;
|
|
12790
13711
|
}
|
|
12791
|
-
iss.crossTreeContext = true;
|
|
12792
13712
|
}
|
|
12793
|
-
if (downgraded > 0) {
|
|
13713
|
+
if (unverified > 0 || downgraded > 0) {
|
|
13714
|
+
const notes = [];
|
|
13715
|
+
if (unverified > 0) {
|
|
13716
|
+
notes.push(
|
|
13717
|
+
`${unverified} cross-tree reference(s) have no vendored surface snapshot covering them and were reported as UNVERIFIED_EXTERNAL_REF warnings \u2014 re-lock the parent so fresh family/sibling snapshots ship, or inspect via \`wairon surface externals\` / sdd_list_external_interfaces.`
|
|
13718
|
+
);
|
|
13719
|
+
}
|
|
13720
|
+
if (downgraded > 0) {
|
|
13721
|
+
notes.push(
|
|
13722
|
+
`${downgraded} code\u2194spec conformance finding(s) (parent-root-relative source paths) were downgraded to warnings.`
|
|
13723
|
+
);
|
|
13724
|
+
}
|
|
12794
13725
|
issues.unshift({
|
|
12795
13726
|
severity: "warning",
|
|
12796
13727
|
code: "CHAINED_SUBPROJECT_CONTEXT",
|
|
12797
13728
|
crossTreeContext: true,
|
|
12798
|
-
message: `This project is a chained subproject ("${chainingParent.subsystemId}") of the parent project at "${chainingParent.parentRoot}". ${
|
|
13729
|
+
message: `This project is a chained subproject ("${chainingParent.subsystemId}") of the parent project at "${chainingParent.parentRoot}". ${notes.join(" ")} Full cross-tree verification runs from the parent root.`
|
|
12799
13730
|
});
|
|
12800
13731
|
}
|
|
12801
13732
|
}
|
|
@@ -12812,7 +13743,7 @@ function validateSddTree(rulesOrOptions, projectType = "backend") {
|
|
|
12812
13743
|
function validateAsComplete(options) {
|
|
12813
13744
|
return validateSddTree({ ...options ?? {}, treatAllAsComplete: true });
|
|
12814
13745
|
}
|
|
12815
|
-
var
|
|
13746
|
+
var SUBPROJECT_REFERENCE_CODES, SUBPROJECT_CONFORMANCE_CODES;
|
|
12816
13747
|
var init_validation = __esm({
|
|
12817
13748
|
"src/core/validation.ts"() {
|
|
12818
13749
|
"use strict";
|
|
@@ -12825,8 +13756,7 @@ var init_validation = __esm({
|
|
|
12825
13756
|
init_source_analysis();
|
|
12826
13757
|
init_specs2();
|
|
12827
13758
|
init_fs();
|
|
12828
|
-
|
|
12829
|
-
// reference resolution
|
|
13759
|
+
SUBPROJECT_REFERENCE_CODES = /* @__PURE__ */ new Set([
|
|
12830
13760
|
"UNDEFINED_TYPE_REFERENCE",
|
|
12831
13761
|
"INVALID_DEPENDENCY_REFERENCE",
|
|
12832
13762
|
"INVALID_TARGET_COMPONENT_REFERENCE",
|
|
@@ -12834,8 +13764,9 @@ var init_validation = __esm({
|
|
|
12834
13764
|
"UNDECLARED_DEPENDENCY_CALL",
|
|
12835
13765
|
"INVALID_TRUSTED_LINK",
|
|
12836
13766
|
"CROSS_SUBSYSTEM_NON_ADAPTER",
|
|
12837
|
-
"CROSS_TREE_REF_UNRESOLVED"
|
|
12838
|
-
|
|
13767
|
+
"CROSS_TREE_REF_UNRESOLVED"
|
|
13768
|
+
]);
|
|
13769
|
+
SUBPROJECT_CONFORMANCE_CODES = /* @__PURE__ */ new Set([
|
|
12839
13770
|
"MISSING_SOURCE_FILE",
|
|
12840
13771
|
"SOURCE_PATH_ESCAPES_ROOT",
|
|
12841
13772
|
"MISSING_SOURCE_PATH",
|
|
@@ -13377,6 +14308,7 @@ __export(specs_exports, {
|
|
|
13377
14308
|
buildProjectGraph: () => buildProjectGraph,
|
|
13378
14309
|
clearLoaderIssues: () => clearLoaderIssues,
|
|
13379
14310
|
collectPromotableSpecs: () => collectPromotableSpecs,
|
|
14311
|
+
computeStateIdAt: () => computeStateIdAt,
|
|
13380
14312
|
deleteComponentSpec: () => deleteComponentSpec,
|
|
13381
14313
|
deleteGroupSpec: () => deleteGroupSpec,
|
|
13382
14314
|
deleteImplementationSpec: () => deleteImplementationSpec,
|
|
@@ -13409,6 +14341,7 @@ __export(specs_exports, {
|
|
|
13409
14341
|
loadTypeSpec: () => loadTypeSpec,
|
|
13410
14342
|
loadTypeSpecs: () => loadTypeSpecs,
|
|
13411
14343
|
normalizeComponentLayout: () => normalizeComponentLayout,
|
|
14344
|
+
resolveChainingParent: () => resolveChainingParent,
|
|
13412
14345
|
resolveSubprojectForNamespace: () => resolveSubprojectForNamespace,
|
|
13413
14346
|
restoreSpecFiles: () => restoreSpecFiles,
|
|
13414
14347
|
saveComponentSpec: () => saveComponentSpec,
|
|
@@ -13813,6 +14746,19 @@ function dryRunSerializeSpecs(include) {
|
|
|
13813
14746
|
function buildProjectGraph(level) {
|
|
13814
14747
|
return buildGraphModel(level);
|
|
13815
14748
|
}
|
|
14749
|
+
function resolveChainingParent() {
|
|
14750
|
+
return findChainingParent(getProjectRoot());
|
|
14751
|
+
}
|
|
14752
|
+
function computeStateIdAt(root) {
|
|
14753
|
+
const resolved = path14.resolve(root);
|
|
14754
|
+
return runWithProjectRoot(resolved, () => {
|
|
14755
|
+
workspaceFor(resolved).invalidate();
|
|
14756
|
+
const system = loadSystemSpec();
|
|
14757
|
+
if (!system) return null;
|
|
14758
|
+
const s = computeStateId();
|
|
14759
|
+
return `${s.algorithm}:${s.digest}`;
|
|
14760
|
+
});
|
|
14761
|
+
}
|
|
13816
14762
|
function deleteTypeSpec(id) {
|
|
13817
14763
|
return current().deleteTypeSpec(id);
|
|
13818
14764
|
}
|
|
@@ -13856,6 +14802,7 @@ var init_specs2 = __esm({
|
|
|
13856
14802
|
path14 = __toESM(require("path"));
|
|
13857
14803
|
init_loader();
|
|
13858
14804
|
init_fs();
|
|
14805
|
+
init_statehash();
|
|
13859
14806
|
init_yaml();
|
|
13860
14807
|
init_models();
|
|
13861
14808
|
init_narrative_labels();
|
|
@@ -16449,6 +17396,9 @@ function requireSpecs() {
|
|
|
16449
17396
|
function requireProvision() {
|
|
16450
17397
|
return init_provision(), __toCommonJS(provision_exports);
|
|
16451
17398
|
}
|
|
17399
|
+
function listExternalInterfaces2() {
|
|
17400
|
+
return listExternalInterfaces();
|
|
17401
|
+
}
|
|
16452
17402
|
function text(content) {
|
|
16453
17403
|
return { content: [{ type: "text", text: content }] };
|
|
16454
17404
|
}
|
|
@@ -17078,6 +18028,7 @@ NOTICE:
|
|
|
17078
18028
|
type: import_zod9.z.enum(["local", "call", "dispatch", "branch", "switch", "loop", "try", "parallel", "jump", "return", "throw"]),
|
|
17079
18029
|
targetComponent: import_zod9.z.string().optional().describe("call/dispatch: L2 component id (for dispatch, the Portal routed through)"),
|
|
17080
18030
|
targetMethod: import_zod9.z.string().optional().describe("call: method name on the target"),
|
|
18031
|
+
auth: import_zod9.z.object({ from: import_zod9.z.string(), note: import_zod9.z.string().optional() }).optional().describe("call/dispatch: the credential this step presents to an AUTHED callee Portal and WHERE it loads from (`from`). Opaque form (env:API_KEY, a config key, vault:path) = a design note wairon never resolves; modeled form `component:<id>` references the Adapter/Store that provides the secret and is validated (must resolve, be an Adapter/Store, and be wired to the presenter). Absence on a call into a Portal whose auth \u2260 none warns (PORTAL_AUTH_UNMET). The authenticated call itself should be made by an Adapter (AUTH_PRESENTER_NOT_ADAPTER)."),
|
|
17081
18032
|
detach: import_zod9.z.boolean().optional().describe("call/dispatch: fire-and-forget \u2014 issue the call and continue without awaiting the result (no later step consumes it)"),
|
|
17082
18033
|
capability: import_zod9.z.string().optional().describe("dispatch: the capability routed through the target Portal's dispatch table (validated against it \u2014 UNSERVED_CAPABILITY)"),
|
|
17083
18034
|
assertsGuarantees: import_zod9.z.array(import_zod9.z.string().min(1)).optional().describe("Semantic guarantees this step relies on \u2014 each must be declared in the called method's L3 guarantees (NARRATIVE_SEMANTIC_UNBACKED otherwise). Builtin tokens: idempotent | atomic | transactional | exactly-once; extension packs may declare more (any other token is UNKNOWN_GUARANTEE)"),
|
|
@@ -17414,7 +18365,36 @@ NOTICE:
|
|
|
17414
18365
|
}
|
|
17415
18366
|
}
|
|
17416
18367
|
);
|
|
18368
|
+
reg(
|
|
18369
|
+
server,
|
|
18370
|
+
"sdd_list_external_interfaces",
|
|
18371
|
+
{
|
|
18372
|
+
description: "List the bound project's consumable external surfaces (parent family, siblings, foreign imports) as discovery entries with origin, provenance, and freshness \u2014 the tool an agent inside a subproject uses to SEE its outward world instead of discovering it by failed reference resolution. Full contracts stay in the vendored snapshots (.wai/surfaces/); each entry summarizes the interface ids it exposes."
|
|
18373
|
+
},
|
|
18374
|
+
() => {
|
|
18375
|
+
try {
|
|
18376
|
+
return json(listExternalInterfaces2());
|
|
18377
|
+
} catch (e) {
|
|
18378
|
+
return errText(String(e));
|
|
18379
|
+
}
|
|
18380
|
+
}
|
|
18381
|
+
);
|
|
17417
18382
|
registerSkillResources(server);
|
|
18383
|
+
try {
|
|
18384
|
+
const chainingParent = resolveChainingParent();
|
|
18385
|
+
if (chainingParent) {
|
|
18386
|
+
let externalSurfaceCount = 0;
|
|
18387
|
+
try {
|
|
18388
|
+
externalSurfaceCount = listExternalInterfaces2().length;
|
|
18389
|
+
} catch {
|
|
18390
|
+
}
|
|
18391
|
+
process.stderr.write(
|
|
18392
|
+
`[wairon mcp] chained subproject: this root is mounted as subsystem "${chainingParent.subsystemId}" of the parent project at ${chainingParent.parentRoot} \u2014 ${externalSurfaceCount} vendored external surface(s) discoverable via sdd_list_external_interfaces
|
|
18393
|
+
`
|
|
18394
|
+
);
|
|
18395
|
+
}
|
|
18396
|
+
} catch {
|
|
18397
|
+
}
|
|
17418
18398
|
if (options.hostedTools) {
|
|
17419
18399
|
const hostedStub = () => errText("This hosted tool is dispatched by the hosting data plane before reaching the MCP server; it is unavailable outside a hosted request.");
|
|
17420
18400
|
reg(server, "sdd_host_lock_project", {
|
|
@@ -17550,6 +18530,8 @@ var init_server = __esm({
|
|
|
17550
18530
|
init_narrative_labels();
|
|
17551
18531
|
init_specs();
|
|
17552
18532
|
init_skills();
|
|
18533
|
+
init_specs2();
|
|
18534
|
+
init_surfaces();
|
|
17553
18535
|
SERVER_BUILD_STAMP = captureBuildStamp(__filename);
|
|
17554
18536
|
STALE_SERVER_WARNING = "\n\n\u26A0 STALE SERVER: the wairon build on disk changed after this MCP server started. Restart the MCP session (e.g. /mcp reconnect) before further spec edits \u2014 writes through a stale server can silently drop fields introduced by newer schemas.";
|
|
17555
18537
|
SKILL_RESOURCE_MIME = "text/markdown";
|
|
@@ -20623,7 +21605,7 @@ var require_dist = __commonJS({
|
|
|
20623
21605
|
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
20624
21606
|
}
|
|
20625
21607
|
var fs51 = __toESM2(require("fs"));
|
|
20626
|
-
var
|
|
21608
|
+
var path61 = __toESM2(require("path"));
|
|
20627
21609
|
var import_fflate = require_node();
|
|
20628
21610
|
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", ".hg", ".svn"]);
|
|
20629
21611
|
function listEntries(archive) {
|
|
@@ -20661,8 +21643,8 @@ var require_dist = __commonJS({
|
|
|
20661
21643
|
}
|
|
20662
21644
|
function writeTree(destDir, files) {
|
|
20663
21645
|
for (const file of files) {
|
|
20664
|
-
const absolute =
|
|
20665
|
-
fs51.mkdirSync(
|
|
21646
|
+
const absolute = path61.join(destDir, file.path);
|
|
21647
|
+
fs51.mkdirSync(path61.dirname(absolute), { recursive: true });
|
|
20666
21648
|
fs51.writeFileSync(absolute, file.contents);
|
|
20667
21649
|
}
|
|
20668
21650
|
}
|
|
@@ -20675,10 +21657,10 @@ var require_dist = __commonJS({
|
|
|
20675
21657
|
for (const entry of fs51.readdirSync(current2, { withFileTypes: true })) {
|
|
20676
21658
|
if (entry.isDirectory()) {
|
|
20677
21659
|
if (SKIP_DIRS.has(entry.name)) continue;
|
|
20678
|
-
walkPackDir(root,
|
|
21660
|
+
walkPackDir(root, path61.join(current2, entry.name), out);
|
|
20679
21661
|
} else if (entry.isFile()) {
|
|
20680
|
-
const absolute =
|
|
20681
|
-
const relative22 =
|
|
21662
|
+
const absolute = path61.join(current2, entry.name);
|
|
21663
|
+
const relative22 = path61.relative(root, absolute).split(path61.sep).join("/");
|
|
20682
21664
|
out.push({ path: relative22, contents: fs51.readFileSync(absolute) });
|
|
20683
21665
|
}
|
|
20684
21666
|
}
|
|
@@ -22089,86 +23071,216 @@ async function generateLayer(options = {}) {
|
|
|
22089
23071
|
}
|
|
22090
23072
|
|
|
22091
23073
|
// src/commands/lock.ts
|
|
23074
|
+
var os7 = __toESM(require("os"));
|
|
22092
23075
|
var import_inquirer2 = __toESM(require("inquirer"));
|
|
22093
23076
|
init_logger();
|
|
22094
|
-
|
|
22095
|
-
|
|
22096
|
-
|
|
22097
|
-
|
|
22098
|
-
|
|
22099
|
-
|
|
22100
|
-
|
|
22101
|
-
|
|
22102
|
-
|
|
23077
|
+
init_defaults();
|
|
23078
|
+
|
|
23079
|
+
// src/core/detection.ts
|
|
23080
|
+
var fs18 = __toESM(require("fs"));
|
|
23081
|
+
var path27 = __toESM(require("path"));
|
|
23082
|
+
init_defaults();
|
|
23083
|
+
var PACKAGE_MARKERS = [
|
|
23084
|
+
"package.json",
|
|
23085
|
+
"pyproject.toml",
|
|
23086
|
+
"Cargo.toml",
|
|
23087
|
+
"go.mod",
|
|
23088
|
+
"build.gradle",
|
|
23089
|
+
"build.gradle.kts",
|
|
23090
|
+
"pom.xml"
|
|
23091
|
+
];
|
|
23092
|
+
var MAX_SCAN_DEPTH = 5;
|
|
23093
|
+
function detectDomainCandidates(projectRoot2, alreadyTrackedPaths = /* @__PURE__ */ new Set(), alreadyTrackedIds = /* @__PURE__ */ new Set()) {
|
|
23094
|
+
const candidates = /* @__PURE__ */ new Map();
|
|
23095
|
+
for (const c of detectGitSubmodules(projectRoot2)) {
|
|
23096
|
+
candidates.set(c.path, { ...c, alreadyTracked: alreadyTrackedPaths.has(c.path) });
|
|
22103
23097
|
}
|
|
22104
|
-
const
|
|
22105
|
-
|
|
22106
|
-
|
|
22107
|
-
const promotable = collectPromotableSpecs(options.subsystem);
|
|
22108
|
-
const originalStatuses = /* @__PURE__ */ new Map();
|
|
22109
|
-
const isSpecInSubsystemScope = (specSubsystem) => {
|
|
22110
|
-
if (!options.subsystem) return true;
|
|
22111
|
-
if (!specSubsystem) return false;
|
|
22112
|
-
return specSubsystem === options.subsystem || specSubsystem.startsWith(`${options.subsystem}::`);
|
|
22113
|
-
};
|
|
22114
|
-
for (const s of index.subsystems) {
|
|
22115
|
-
if (!options.subsystem || s.id === options.subsystem || s.id.startsWith(`${options.subsystem}::`)) {
|
|
22116
|
-
originalStatuses.set(s, s.status);
|
|
22117
|
-
s.status = "complete";
|
|
23098
|
+
for (const c of detectNestedGitRepos(projectRoot2)) {
|
|
23099
|
+
if (!candidates.has(c.path)) {
|
|
23100
|
+
candidates.set(c.path, { ...c, alreadyTracked: alreadyTrackedPaths.has(c.path) });
|
|
22118
23101
|
}
|
|
22119
23102
|
}
|
|
22120
|
-
|
|
22121
|
-
|
|
22122
|
-
|
|
22123
|
-
|
|
22124
|
-
|
|
23103
|
+
const gitPaths = new Set(
|
|
23104
|
+
Array.from(candidates.values()).filter((c) => c.type === "git-submodule" || c.type === "git-repo").map((c) => c.path)
|
|
23105
|
+
);
|
|
23106
|
+
for (const c of detectPackageRoots(projectRoot2)) {
|
|
23107
|
+
if (candidates.has(c.path)) continue;
|
|
23108
|
+
const insideGit = Array.from(gitPaths).some(
|
|
23109
|
+
(gp) => c.path === gp || c.path.startsWith(gp + "/")
|
|
23110
|
+
);
|
|
23111
|
+
if (insideGit) continue;
|
|
23112
|
+
candidates.set(c.path, { ...c, alreadyTracked: alreadyTrackedPaths.has(c.path) });
|
|
22125
23113
|
}
|
|
22126
|
-
|
|
22127
|
-
|
|
22128
|
-
|
|
22129
|
-
|
|
22130
|
-
|
|
22131
|
-
|
|
23114
|
+
const sorted = Array.from(candidates.values()).sort((a, b) => a.path.localeCompare(b.path));
|
|
23115
|
+
return deduplicateIds(sorted, alreadyTrackedIds);
|
|
23116
|
+
}
|
|
23117
|
+
function deduplicateIds(candidates, existingIds = /* @__PURE__ */ new Set()) {
|
|
23118
|
+
const idCount = /* @__PURE__ */ new Map();
|
|
23119
|
+
for (const id of existingIds) {
|
|
23120
|
+
idCount.set(id, (idCount.get(id) ?? 0) + 1);
|
|
22132
23121
|
}
|
|
22133
|
-
for (const
|
|
22134
|
-
|
|
22135
|
-
const comp = intf ? index.components.find((c) => c.id === intf.component) : null;
|
|
22136
|
-
if (comp && isSpecInSubsystemScope(comp.subsystem)) {
|
|
22137
|
-
originalStatuses.set(m, m.status);
|
|
22138
|
-
m.status = "complete";
|
|
22139
|
-
}
|
|
23122
|
+
for (const c of candidates) {
|
|
23123
|
+
idCount.set(c.suggestedId, (idCount.get(c.suggestedId) ?? 0) + 1);
|
|
22140
23124
|
}
|
|
22141
|
-
|
|
22142
|
-
|
|
22143
|
-
|
|
22144
|
-
|
|
22145
|
-
|
|
23125
|
+
return candidates.map((c) => {
|
|
23126
|
+
if ((idCount.get(c.suggestedId) ?? 0) <= 1) return c;
|
|
23127
|
+
const parts = c.path.split("/");
|
|
23128
|
+
const qualifiedId2 = parts.length >= 2 ? pathToId(`${parts[parts.length - 2]}-${parts[parts.length - 1]}`) : c.suggestedId;
|
|
23129
|
+
return { ...c, suggestedId: qualifiedId2 };
|
|
22146
23130
|
});
|
|
22147
|
-
|
|
22148
|
-
|
|
23131
|
+
}
|
|
23132
|
+
function parseGitmodules(filePath) {
|
|
23133
|
+
const content = fs18.readFileSync(filePath, "utf-8");
|
|
23134
|
+
const entries = [];
|
|
23135
|
+
let current2 = {};
|
|
23136
|
+
for (const line2 of content.split("\n")) {
|
|
23137
|
+
const trimmed = line2.trim();
|
|
23138
|
+
const headerMatch = trimmed.match(/^\[submodule "(.+)"\]$/);
|
|
23139
|
+
if (headerMatch) {
|
|
23140
|
+
if (current2.path) entries.push(current2);
|
|
23141
|
+
current2 = { name: headerMatch[1] };
|
|
23142
|
+
continue;
|
|
23143
|
+
}
|
|
23144
|
+
const keyVal = trimmed.match(/^(\w+)\s*=\s*(.+)$/);
|
|
23145
|
+
if (keyVal) {
|
|
23146
|
+
const [, key, value] = keyVal;
|
|
23147
|
+
if (key === "path") current2.path = value.trim();
|
|
23148
|
+
if (key === "url") current2.url = value.trim();
|
|
23149
|
+
}
|
|
22149
23150
|
}
|
|
22150
|
-
|
|
22151
|
-
|
|
22152
|
-
|
|
22153
|
-
|
|
22154
|
-
|
|
22155
|
-
|
|
22156
|
-
|
|
22157
|
-
|
|
22158
|
-
|
|
22159
|
-
|
|
22160
|
-
|
|
22161
|
-
|
|
22162
|
-
|
|
23151
|
+
if (current2.path) entries.push(current2);
|
|
23152
|
+
return entries;
|
|
23153
|
+
}
|
|
23154
|
+
function detectGitSubmodules(projectRoot2) {
|
|
23155
|
+
const gitmodulesPath = path27.join(projectRoot2, ".gitmodules");
|
|
23156
|
+
if (!fs18.existsSync(gitmodulesPath)) return [];
|
|
23157
|
+
return parseGitmodules(gitmodulesPath).map((entry) => ({
|
|
23158
|
+
suggestedId: pathToId(entry.path),
|
|
23159
|
+
suggestedName: pathToName(entry.path),
|
|
23160
|
+
path: normalizePath3(entry.path),
|
|
23161
|
+
type: "git-submodule",
|
|
23162
|
+
alreadyTracked: false
|
|
23163
|
+
}));
|
|
23164
|
+
}
|
|
23165
|
+
function detectNestedGitRepos(projectRoot2) {
|
|
23166
|
+
const results = [];
|
|
23167
|
+
walkForGit(projectRoot2, projectRoot2, 0, results);
|
|
23168
|
+
return results;
|
|
23169
|
+
}
|
|
23170
|
+
function walkForGit(projectRoot2, currentDir, depth, results) {
|
|
23171
|
+
if (depth > MAX_SCAN_DEPTH) return;
|
|
23172
|
+
let entries;
|
|
23173
|
+
try {
|
|
23174
|
+
entries = fs18.readdirSync(currentDir, { withFileTypes: true });
|
|
23175
|
+
} catch {
|
|
23176
|
+
return;
|
|
23177
|
+
}
|
|
23178
|
+
for (const entry of entries) {
|
|
23179
|
+
if (!entry.isDirectory()) continue;
|
|
23180
|
+
if (SCAN_EXCLUDE_DIRS.has(entry.name)) continue;
|
|
23181
|
+
const fullPath = path27.join(currentDir, entry.name);
|
|
23182
|
+
const relPath = normalizePath3(path27.relative(projectRoot2, fullPath));
|
|
23183
|
+
if (relPath === "" || relPath === ".") continue;
|
|
23184
|
+
const gitPath = path27.join(fullPath, ".git");
|
|
23185
|
+
if (fs18.existsSync(gitPath)) {
|
|
23186
|
+
results.push({
|
|
23187
|
+
suggestedId: pathToId(relPath),
|
|
23188
|
+
suggestedName: pathToName(relPath),
|
|
23189
|
+
path: relPath,
|
|
23190
|
+
type: "git-repo",
|
|
23191
|
+
alreadyTracked: false
|
|
23192
|
+
});
|
|
23193
|
+
continue;
|
|
22163
23194
|
}
|
|
22164
|
-
|
|
22165
|
-
|
|
23195
|
+
walkForGit(projectRoot2, fullPath, depth + 1, results);
|
|
23196
|
+
}
|
|
23197
|
+
}
|
|
23198
|
+
function detectPackageRoots(projectRoot2) {
|
|
23199
|
+
const results = [];
|
|
23200
|
+
walkForPackages(projectRoot2, projectRoot2, 0, results);
|
|
23201
|
+
return results;
|
|
23202
|
+
}
|
|
23203
|
+
function walkForPackages(projectRoot2, currentDir, depth, results) {
|
|
23204
|
+
if (depth > MAX_SCAN_DEPTH) return;
|
|
23205
|
+
let entries;
|
|
23206
|
+
try {
|
|
23207
|
+
entries = fs18.readdirSync(currentDir, { withFileTypes: true });
|
|
23208
|
+
} catch {
|
|
23209
|
+
return;
|
|
23210
|
+
}
|
|
23211
|
+
for (const entry of entries) {
|
|
23212
|
+
if (!entry.isDirectory()) continue;
|
|
23213
|
+
if (SCAN_EXCLUDE_DIRS.has(entry.name)) continue;
|
|
23214
|
+
const fullPath = path27.join(currentDir, entry.name);
|
|
23215
|
+
const relPath = normalizePath3(path27.relative(projectRoot2, fullPath));
|
|
23216
|
+
if (relPath === "" || relPath === ".") continue;
|
|
23217
|
+
const hasMarker = PACKAGE_MARKERS.some((m) => fs18.existsSync(path27.join(fullPath, m)));
|
|
23218
|
+
if (hasMarker) {
|
|
23219
|
+
results.push({
|
|
23220
|
+
suggestedId: pathToId(relPath),
|
|
23221
|
+
suggestedName: pathToName(relPath),
|
|
23222
|
+
path: relPath,
|
|
23223
|
+
type: "package-root",
|
|
23224
|
+
alreadyTracked: false
|
|
23225
|
+
});
|
|
22166
23226
|
}
|
|
22167
|
-
|
|
22168
|
-
logger.info("Fix the errors above, then run `wairon lock` again. Nothing was changed.");
|
|
22169
|
-
process.exit(1);
|
|
23227
|
+
walkForPackages(projectRoot2, fullPath, depth + 1, results);
|
|
22170
23228
|
}
|
|
22171
|
-
|
|
23229
|
+
}
|
|
23230
|
+
function pathToId(relPath) {
|
|
23231
|
+
const basename11 = path27.basename(relPath);
|
|
23232
|
+
return basename11.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
23233
|
+
}
|
|
23234
|
+
function pathToName(relPath) {
|
|
23235
|
+
const id = pathToId(relPath);
|
|
23236
|
+
return id.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
23237
|
+
}
|
|
23238
|
+
function normalizePath3(p) {
|
|
23239
|
+
return p.replace(/\\/g, "/");
|
|
23240
|
+
}
|
|
23241
|
+
|
|
23242
|
+
// src/core/index.ts
|
|
23243
|
+
init_domains();
|
|
23244
|
+
init_validation();
|
|
23245
|
+
init_extensions();
|
|
23246
|
+
init_variants();
|
|
23247
|
+
init_rules();
|
|
23248
|
+
init_specs2();
|
|
23249
|
+
init_provision();
|
|
23250
|
+
init_diagram();
|
|
23251
|
+
|
|
23252
|
+
// src/core/lockfile.ts
|
|
23253
|
+
var fs19 = __toESM(require("fs"));
|
|
23254
|
+
var path28 = __toESM(require("path"));
|
|
23255
|
+
init_fs();
|
|
23256
|
+
function lockPath() {
|
|
23257
|
+
return aiDir("lock.json");
|
|
23258
|
+
}
|
|
23259
|
+
function readLockRecord() {
|
|
23260
|
+
try {
|
|
23261
|
+
return JSON.parse(fs19.readFileSync(lockPath(), "utf8"));
|
|
23262
|
+
} catch {
|
|
23263
|
+
return null;
|
|
23264
|
+
}
|
|
23265
|
+
}
|
|
23266
|
+
function writeLockRecord(record2) {
|
|
23267
|
+
const p = lockPath();
|
|
23268
|
+
fs19.mkdirSync(path28.dirname(p), { recursive: true });
|
|
23269
|
+
const tmp = `${p}.tmp`;
|
|
23270
|
+
fs19.writeFileSync(tmp, JSON.stringify(record2, null, 2) + "\n");
|
|
23271
|
+
fs19.renameSync(tmp, p);
|
|
23272
|
+
}
|
|
23273
|
+
|
|
23274
|
+
// src/core/index.ts
|
|
23275
|
+
init_statehash();
|
|
23276
|
+
init_agent_resolver();
|
|
23277
|
+
init_skills();
|
|
23278
|
+
init_surfaces();
|
|
23279
|
+
init_openapi();
|
|
23280
|
+
|
|
23281
|
+
// src/commands/lock.ts
|
|
23282
|
+
async function runLock(options = {}, gate) {
|
|
23283
|
+
const promotable = collectPromotableSpecs(options.subsystem);
|
|
22172
23284
|
if (promotable.length === 0) {
|
|
22173
23285
|
logger.info("All specs are already complete \u2014 this will re-validate and regenerate the agent topology.");
|
|
22174
23286
|
} else {
|
|
@@ -22192,23 +23304,36 @@ async function runLock(options = {}) {
|
|
|
22192
23304
|
default: false
|
|
22193
23305
|
}
|
|
22194
23306
|
]);
|
|
22195
|
-
if (!confirmed)
|
|
22196
|
-
|
|
22197
|
-
|
|
22198
|
-
|
|
23307
|
+
if (!confirmed) return null;
|
|
23308
|
+
}
|
|
23309
|
+
if (options.subsystem) {
|
|
23310
|
+
for (const p of promotable) applySpecStatus(p.kind, p.id, "complete");
|
|
23311
|
+
invalidateSpecCache();
|
|
23312
|
+
} else {
|
|
23313
|
+
promoteAllComplete();
|
|
22199
23314
|
}
|
|
22200
|
-
for (const p of promotable) applySpecStatus(p.kind, p.id, "complete");
|
|
22201
|
-
invalidateSpecCache();
|
|
22202
23315
|
if (promotable.length > 0) {
|
|
22203
23316
|
logger.success(`Locked ${promotable.length} spec(s) as complete.`);
|
|
22204
23317
|
}
|
|
22205
|
-
|
|
22206
|
-
|
|
22207
|
-
|
|
22208
|
-
|
|
22209
|
-
|
|
22210
|
-
|
|
22211
|
-
|
|
23318
|
+
let lockedBy = "local";
|
|
23319
|
+
try {
|
|
23320
|
+
lockedBy = `local:${os7.userInfo().username}`;
|
|
23321
|
+
} catch {
|
|
23322
|
+
}
|
|
23323
|
+
const record2 = {
|
|
23324
|
+
stateId: computeStateId(),
|
|
23325
|
+
lockedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
23326
|
+
lockedBy,
|
|
23327
|
+
validatorVersion: WAIRON_VERSION,
|
|
23328
|
+
validationResult: {
|
|
23329
|
+
valid: true,
|
|
23330
|
+
errors: 0,
|
|
23331
|
+
warnings: gate ? gate.issues.filter((i) => i.severity === "warning").length : 0
|
|
23332
|
+
},
|
|
23333
|
+
status: "ready"
|
|
23334
|
+
};
|
|
23335
|
+
writeLockRecord(record2);
|
|
23336
|
+
return record2;
|
|
22212
23337
|
}
|
|
22213
23338
|
|
|
22214
23339
|
// src/commands/validate.ts
|
|
@@ -22347,6 +23472,10 @@ async function runValidate(options = {}) {
|
|
|
22347
23472
|
}
|
|
22348
23473
|
}
|
|
22349
23474
|
|
|
23475
|
+
// src/cli/index.ts
|
|
23476
|
+
init_loader();
|
|
23477
|
+
init_fs();
|
|
23478
|
+
|
|
22350
23479
|
// src/commands/list.ts
|
|
22351
23480
|
var import_chalk7 = __toESM(require("chalk"));
|
|
22352
23481
|
init_logger();
|
|
@@ -22461,9 +23590,9 @@ init_mcp();
|
|
|
22461
23590
|
// src/commands/update.ts
|
|
22462
23591
|
var https = __toESM(require("https"));
|
|
22463
23592
|
var http = __toESM(require("http"));
|
|
22464
|
-
var
|
|
22465
|
-
var
|
|
22466
|
-
var
|
|
23593
|
+
var fs20 = __toESM(require("fs"));
|
|
23594
|
+
var path29 = __toESM(require("path"));
|
|
23595
|
+
var os8 = __toESM(require("os"));
|
|
22467
23596
|
var crypto2 = __toESM(require("crypto"));
|
|
22468
23597
|
var import_child_process2 = require("child_process");
|
|
22469
23598
|
init_logger();
|
|
@@ -22526,8 +23655,8 @@ async function runUpdate(options = {}) {
|
|
|
22526
23655
|
logger.info(`Download manually from: ${release.html_url}`);
|
|
22527
23656
|
process.exit(1);
|
|
22528
23657
|
}
|
|
22529
|
-
const tmpDir =
|
|
22530
|
-
const tmpFile =
|
|
23658
|
+
const tmpDir = os8.tmpdir();
|
|
23659
|
+
const tmpFile = path29.join(tmpDir, assetName);
|
|
22531
23660
|
logger.info(`Downloading ${assetName}...`);
|
|
22532
23661
|
try {
|
|
22533
23662
|
await downloadFile(asset.browser_download_url, tmpFile);
|
|
@@ -22544,16 +23673,16 @@ async function runUpdate(options = {}) {
|
|
|
22544
23673
|
const checksumAssetName = assetName + ".sha256";
|
|
22545
23674
|
const checksumAsset = release.assets.find((a) => a.name === checksumAssetName);
|
|
22546
23675
|
if (checksumAsset) {
|
|
22547
|
-
const tmpChecksum =
|
|
23676
|
+
const tmpChecksum = path29.join(tmpDir, checksumAssetName);
|
|
22548
23677
|
logger.info(`Verifying checksum...`);
|
|
22549
23678
|
try {
|
|
22550
23679
|
await downloadFile(checksumAsset.browser_download_url, tmpChecksum);
|
|
22551
23680
|
verifyChecksum(tmpFile, tmpChecksum, assetName);
|
|
22552
|
-
|
|
23681
|
+
fs20.unlinkSync(tmpChecksum);
|
|
22553
23682
|
} catch (err) {
|
|
22554
23683
|
logger.error(`Checksum verification failed: ${err.message}`);
|
|
22555
23684
|
try {
|
|
22556
|
-
|
|
23685
|
+
fs20.unlinkSync(tmpFile);
|
|
22557
23686
|
} catch {
|
|
22558
23687
|
}
|
|
22559
23688
|
process.exit(1);
|
|
@@ -22619,7 +23748,7 @@ function fetchReleases(repo) {
|
|
|
22619
23748
|
}
|
|
22620
23749
|
function downloadFile(url, dest) {
|
|
22621
23750
|
return new Promise((resolve24, reject) => {
|
|
22622
|
-
const file =
|
|
23751
|
+
const file = fs20.createWriteStream(dest);
|
|
22623
23752
|
const get3 = url.startsWith("https://") ? https.get : http.get;
|
|
22624
23753
|
get3(url, { headers: { "User-Agent": `wairon/${WAIRON_VERSION}` }, agent: false }, (res) => {
|
|
22625
23754
|
if (res.statusCode === 301 || res.statusCode === 302) {
|
|
@@ -22641,21 +23770,21 @@ function downloadFile(url, dest) {
|
|
|
22641
23770
|
});
|
|
22642
23771
|
file.on("error", (err) => {
|
|
22643
23772
|
res.destroy();
|
|
22644
|
-
|
|
23773
|
+
fs20.unlink(dest, () => {
|
|
22645
23774
|
});
|
|
22646
23775
|
reject(err);
|
|
22647
23776
|
});
|
|
22648
23777
|
}).on("error", (err) => {
|
|
22649
|
-
|
|
23778
|
+
fs20.unlink(dest, () => {
|
|
22650
23779
|
});
|
|
22651
23780
|
reject(err);
|
|
22652
23781
|
});
|
|
22653
23782
|
});
|
|
22654
23783
|
}
|
|
22655
23784
|
function verifyChecksum(filePath, checksumFile, expectedFilename) {
|
|
22656
|
-
const checksumContent =
|
|
23785
|
+
const checksumContent = fs20.readFileSync(checksumFile, "utf-8").trim();
|
|
22657
23786
|
const expectedHash = checksumContent.split(/\s+/)[0].toLowerCase();
|
|
22658
|
-
const fileBuffer =
|
|
23787
|
+
const fileBuffer = fs20.readFileSync(filePath);
|
|
22659
23788
|
const actualHash = crypto2.createHash("sha256").update(fileBuffer).digest("hex").toLowerCase();
|
|
22660
23789
|
if (actualHash !== expectedHash) {
|
|
22661
23790
|
throw new Error(
|
|
@@ -22686,9 +23815,9 @@ function isPkgBinary2() {
|
|
|
22686
23815
|
function installBinary(tmpFile, destPath) {
|
|
22687
23816
|
const platform = process.platform;
|
|
22688
23817
|
const isZip = tmpFile.endsWith(".zip");
|
|
22689
|
-
const extractDir =
|
|
22690
|
-
if (
|
|
22691
|
-
|
|
23818
|
+
const extractDir = path29.join(os8.tmpdir(), "wairon-extract");
|
|
23819
|
+
if (fs20.existsSync(extractDir)) fs20.rmSync(extractDir, { recursive: true });
|
|
23820
|
+
fs20.mkdirSync(extractDir, { recursive: true });
|
|
22692
23821
|
if (isZip) {
|
|
22693
23822
|
(0, import_child_process2.execSync)(
|
|
22694
23823
|
`powershell -NoProfile -NonInteractive -Command "Expand-Archive -Path '${tmpFile}' -DestinationPath '${extractDir}' -Force"`,
|
|
@@ -22698,18 +23827,18 @@ function installBinary(tmpFile, destPath) {
|
|
|
22698
23827
|
(0, import_child_process2.execSync)(`tar -xzf "${tmpFile}" -C "${extractDir}"`, { stdio: ["ignore", "pipe", "pipe"] });
|
|
22699
23828
|
}
|
|
22700
23829
|
const binaryName = platform === "win32" ? "wairon.exe" : "wairon";
|
|
22701
|
-
const extractedBinary =
|
|
22702
|
-
if (!
|
|
23830
|
+
const extractedBinary = path29.join(extractDir, binaryName);
|
|
23831
|
+
if (!fs20.existsSync(extractedBinary)) {
|
|
22703
23832
|
throw new Error(`Extracted binary not found at ${extractedBinary}`);
|
|
22704
23833
|
}
|
|
22705
23834
|
if (platform === "win32") {
|
|
22706
23835
|
const oldPath = destPath + ".old";
|
|
22707
23836
|
try {
|
|
22708
23837
|
cleanStaleBinary(oldPath);
|
|
22709
|
-
|
|
22710
|
-
|
|
23838
|
+
fs20.renameSync(destPath, oldPath);
|
|
23839
|
+
fs20.copyFileSync(extractedBinary, destPath);
|
|
22711
23840
|
try {
|
|
22712
|
-
|
|
23841
|
+
fs20.unlinkSync(oldPath);
|
|
22713
23842
|
} catch {
|
|
22714
23843
|
}
|
|
22715
23844
|
} catch (err) {
|
|
@@ -22723,25 +23852,25 @@ function installBinary(tmpFile, destPath) {
|
|
|
22723
23852
|
}
|
|
22724
23853
|
} else {
|
|
22725
23854
|
const tmpDest = destPath + ".new";
|
|
22726
|
-
|
|
22727
|
-
|
|
22728
|
-
|
|
23855
|
+
fs20.copyFileSync(extractedBinary, tmpDest);
|
|
23856
|
+
fs20.chmodSync(tmpDest, 493);
|
|
23857
|
+
fs20.renameSync(tmpDest, destPath);
|
|
22729
23858
|
}
|
|
22730
23859
|
try {
|
|
22731
|
-
|
|
23860
|
+
fs20.unlinkSync(tmpFile);
|
|
22732
23861
|
} catch {
|
|
22733
23862
|
}
|
|
22734
23863
|
try {
|
|
22735
|
-
|
|
23864
|
+
fs20.rmSync(extractDir, { recursive: true });
|
|
22736
23865
|
} catch {
|
|
22737
23866
|
}
|
|
22738
23867
|
}
|
|
22739
23868
|
function cleanStaleBinary(oldPath) {
|
|
22740
23869
|
const target = oldPath ?? (isPkgBinary2() ? process.execPath + ".old" : null);
|
|
22741
23870
|
if (!target) return;
|
|
22742
|
-
if (
|
|
23871
|
+
if (fs20.existsSync(target)) {
|
|
22743
23872
|
try {
|
|
22744
|
-
|
|
23873
|
+
fs20.unlinkSync(target);
|
|
22745
23874
|
} catch {
|
|
22746
23875
|
}
|
|
22747
23876
|
}
|
|
@@ -22987,171 +24116,6 @@ async function filteredCheckbox(config) {
|
|
|
22987
24116
|
|
|
22988
24117
|
// src/commands/domains.ts
|
|
22989
24118
|
init_loader();
|
|
22990
|
-
|
|
22991
|
-
// src/core/detection.ts
|
|
22992
|
-
var fs19 = __toESM(require("fs"));
|
|
22993
|
-
var path28 = __toESM(require("path"));
|
|
22994
|
-
init_defaults();
|
|
22995
|
-
var PACKAGE_MARKERS = [
|
|
22996
|
-
"package.json",
|
|
22997
|
-
"pyproject.toml",
|
|
22998
|
-
"Cargo.toml",
|
|
22999
|
-
"go.mod",
|
|
23000
|
-
"build.gradle",
|
|
23001
|
-
"build.gradle.kts",
|
|
23002
|
-
"pom.xml"
|
|
23003
|
-
];
|
|
23004
|
-
var MAX_SCAN_DEPTH = 5;
|
|
23005
|
-
function detectDomainCandidates(projectRoot2, alreadyTrackedPaths = /* @__PURE__ */ new Set(), alreadyTrackedIds = /* @__PURE__ */ new Set()) {
|
|
23006
|
-
const candidates = /* @__PURE__ */ new Map();
|
|
23007
|
-
for (const c of detectGitSubmodules(projectRoot2)) {
|
|
23008
|
-
candidates.set(c.path, { ...c, alreadyTracked: alreadyTrackedPaths.has(c.path) });
|
|
23009
|
-
}
|
|
23010
|
-
for (const c of detectNestedGitRepos(projectRoot2)) {
|
|
23011
|
-
if (!candidates.has(c.path)) {
|
|
23012
|
-
candidates.set(c.path, { ...c, alreadyTracked: alreadyTrackedPaths.has(c.path) });
|
|
23013
|
-
}
|
|
23014
|
-
}
|
|
23015
|
-
const gitPaths = new Set(
|
|
23016
|
-
Array.from(candidates.values()).filter((c) => c.type === "git-submodule" || c.type === "git-repo").map((c) => c.path)
|
|
23017
|
-
);
|
|
23018
|
-
for (const c of detectPackageRoots(projectRoot2)) {
|
|
23019
|
-
if (candidates.has(c.path)) continue;
|
|
23020
|
-
const insideGit = Array.from(gitPaths).some(
|
|
23021
|
-
(gp) => c.path === gp || c.path.startsWith(gp + "/")
|
|
23022
|
-
);
|
|
23023
|
-
if (insideGit) continue;
|
|
23024
|
-
candidates.set(c.path, { ...c, alreadyTracked: alreadyTrackedPaths.has(c.path) });
|
|
23025
|
-
}
|
|
23026
|
-
const sorted = Array.from(candidates.values()).sort((a, b) => a.path.localeCompare(b.path));
|
|
23027
|
-
return deduplicateIds(sorted, alreadyTrackedIds);
|
|
23028
|
-
}
|
|
23029
|
-
function deduplicateIds(candidates, existingIds = /* @__PURE__ */ new Set()) {
|
|
23030
|
-
const idCount = /* @__PURE__ */ new Map();
|
|
23031
|
-
for (const id of existingIds) {
|
|
23032
|
-
idCount.set(id, (idCount.get(id) ?? 0) + 1);
|
|
23033
|
-
}
|
|
23034
|
-
for (const c of candidates) {
|
|
23035
|
-
idCount.set(c.suggestedId, (idCount.get(c.suggestedId) ?? 0) + 1);
|
|
23036
|
-
}
|
|
23037
|
-
return candidates.map((c) => {
|
|
23038
|
-
if ((idCount.get(c.suggestedId) ?? 0) <= 1) return c;
|
|
23039
|
-
const parts = c.path.split("/");
|
|
23040
|
-
const qualifiedId2 = parts.length >= 2 ? pathToId(`${parts[parts.length - 2]}-${parts[parts.length - 1]}`) : c.suggestedId;
|
|
23041
|
-
return { ...c, suggestedId: qualifiedId2 };
|
|
23042
|
-
});
|
|
23043
|
-
}
|
|
23044
|
-
function parseGitmodules(filePath) {
|
|
23045
|
-
const content = fs19.readFileSync(filePath, "utf-8");
|
|
23046
|
-
const entries = [];
|
|
23047
|
-
let current2 = {};
|
|
23048
|
-
for (const line2 of content.split("\n")) {
|
|
23049
|
-
const trimmed = line2.trim();
|
|
23050
|
-
const headerMatch = trimmed.match(/^\[submodule "(.+)"\]$/);
|
|
23051
|
-
if (headerMatch) {
|
|
23052
|
-
if (current2.path) entries.push(current2);
|
|
23053
|
-
current2 = { name: headerMatch[1] };
|
|
23054
|
-
continue;
|
|
23055
|
-
}
|
|
23056
|
-
const keyVal = trimmed.match(/^(\w+)\s*=\s*(.+)$/);
|
|
23057
|
-
if (keyVal) {
|
|
23058
|
-
const [, key, value] = keyVal;
|
|
23059
|
-
if (key === "path") current2.path = value.trim();
|
|
23060
|
-
if (key === "url") current2.url = value.trim();
|
|
23061
|
-
}
|
|
23062
|
-
}
|
|
23063
|
-
if (current2.path) entries.push(current2);
|
|
23064
|
-
return entries;
|
|
23065
|
-
}
|
|
23066
|
-
function detectGitSubmodules(projectRoot2) {
|
|
23067
|
-
const gitmodulesPath = path28.join(projectRoot2, ".gitmodules");
|
|
23068
|
-
if (!fs19.existsSync(gitmodulesPath)) return [];
|
|
23069
|
-
return parseGitmodules(gitmodulesPath).map((entry) => ({
|
|
23070
|
-
suggestedId: pathToId(entry.path),
|
|
23071
|
-
suggestedName: pathToName(entry.path),
|
|
23072
|
-
path: normalizePath3(entry.path),
|
|
23073
|
-
type: "git-submodule",
|
|
23074
|
-
alreadyTracked: false
|
|
23075
|
-
}));
|
|
23076
|
-
}
|
|
23077
|
-
function detectNestedGitRepos(projectRoot2) {
|
|
23078
|
-
const results = [];
|
|
23079
|
-
walkForGit(projectRoot2, projectRoot2, 0, results);
|
|
23080
|
-
return results;
|
|
23081
|
-
}
|
|
23082
|
-
function walkForGit(projectRoot2, currentDir, depth, results) {
|
|
23083
|
-
if (depth > MAX_SCAN_DEPTH) return;
|
|
23084
|
-
let entries;
|
|
23085
|
-
try {
|
|
23086
|
-
entries = fs19.readdirSync(currentDir, { withFileTypes: true });
|
|
23087
|
-
} catch {
|
|
23088
|
-
return;
|
|
23089
|
-
}
|
|
23090
|
-
for (const entry of entries) {
|
|
23091
|
-
if (!entry.isDirectory()) continue;
|
|
23092
|
-
if (SCAN_EXCLUDE_DIRS.has(entry.name)) continue;
|
|
23093
|
-
const fullPath = path28.join(currentDir, entry.name);
|
|
23094
|
-
const relPath = normalizePath3(path28.relative(projectRoot2, fullPath));
|
|
23095
|
-
if (relPath === "" || relPath === ".") continue;
|
|
23096
|
-
const gitPath = path28.join(fullPath, ".git");
|
|
23097
|
-
if (fs19.existsSync(gitPath)) {
|
|
23098
|
-
results.push({
|
|
23099
|
-
suggestedId: pathToId(relPath),
|
|
23100
|
-
suggestedName: pathToName(relPath),
|
|
23101
|
-
path: relPath,
|
|
23102
|
-
type: "git-repo",
|
|
23103
|
-
alreadyTracked: false
|
|
23104
|
-
});
|
|
23105
|
-
continue;
|
|
23106
|
-
}
|
|
23107
|
-
walkForGit(projectRoot2, fullPath, depth + 1, results);
|
|
23108
|
-
}
|
|
23109
|
-
}
|
|
23110
|
-
function detectPackageRoots(projectRoot2) {
|
|
23111
|
-
const results = [];
|
|
23112
|
-
walkForPackages(projectRoot2, projectRoot2, 0, results);
|
|
23113
|
-
return results;
|
|
23114
|
-
}
|
|
23115
|
-
function walkForPackages(projectRoot2, currentDir, depth, results) {
|
|
23116
|
-
if (depth > MAX_SCAN_DEPTH) return;
|
|
23117
|
-
let entries;
|
|
23118
|
-
try {
|
|
23119
|
-
entries = fs19.readdirSync(currentDir, { withFileTypes: true });
|
|
23120
|
-
} catch {
|
|
23121
|
-
return;
|
|
23122
|
-
}
|
|
23123
|
-
for (const entry of entries) {
|
|
23124
|
-
if (!entry.isDirectory()) continue;
|
|
23125
|
-
if (SCAN_EXCLUDE_DIRS.has(entry.name)) continue;
|
|
23126
|
-
const fullPath = path28.join(currentDir, entry.name);
|
|
23127
|
-
const relPath = normalizePath3(path28.relative(projectRoot2, fullPath));
|
|
23128
|
-
if (relPath === "" || relPath === ".") continue;
|
|
23129
|
-
const hasMarker = PACKAGE_MARKERS.some((m) => fs19.existsSync(path28.join(fullPath, m)));
|
|
23130
|
-
if (hasMarker) {
|
|
23131
|
-
results.push({
|
|
23132
|
-
suggestedId: pathToId(relPath),
|
|
23133
|
-
suggestedName: pathToName(relPath),
|
|
23134
|
-
path: relPath,
|
|
23135
|
-
type: "package-root",
|
|
23136
|
-
alreadyTracked: false
|
|
23137
|
-
});
|
|
23138
|
-
}
|
|
23139
|
-
walkForPackages(projectRoot2, fullPath, depth + 1, results);
|
|
23140
|
-
}
|
|
23141
|
-
}
|
|
23142
|
-
function pathToId(relPath) {
|
|
23143
|
-
const basename12 = path28.basename(relPath);
|
|
23144
|
-
return basename12.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
23145
|
-
}
|
|
23146
|
-
function pathToName(relPath) {
|
|
23147
|
-
const id = pathToId(relPath);
|
|
23148
|
-
return id.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
23149
|
-
}
|
|
23150
|
-
function normalizePath3(p) {
|
|
23151
|
-
return p.replace(/\\/g, "/");
|
|
23152
|
-
}
|
|
23153
|
-
|
|
23154
|
-
// src/commands/domains.ts
|
|
23155
24119
|
init_domains();
|
|
23156
24120
|
init_domain();
|
|
23157
24121
|
async function runDomainsList() {
|
|
@@ -23341,9 +24305,9 @@ async function runSkillsInstall() {
|
|
|
23341
24305
|
}
|
|
23342
24306
|
|
|
23343
24307
|
// src/commands/doctor.ts
|
|
23344
|
-
var
|
|
23345
|
-
var
|
|
23346
|
-
var
|
|
24308
|
+
var fs21 = __toESM(require("fs"));
|
|
24309
|
+
var os9 = __toESM(require("os"));
|
|
24310
|
+
var path30 = __toESM(require("path"));
|
|
23347
24311
|
var import_chalk12 = __toESM(require("chalk"));
|
|
23348
24312
|
init_logger();
|
|
23349
24313
|
init_defaults();
|
|
@@ -23373,10 +24337,10 @@ function stampVerdict(content) {
|
|
|
23373
24337
|
return { mark: "warn", note: `v${v} \u2014 stale, installed is v${WAIRON_VERSION}` };
|
|
23374
24338
|
}
|
|
23375
24339
|
function mcpEntryHealth(settingsPath) {
|
|
23376
|
-
if (!
|
|
24340
|
+
if (!fs21.existsSync(settingsPath)) return { mark: "warn", note: "not registered" };
|
|
23377
24341
|
let entry;
|
|
23378
24342
|
try {
|
|
23379
|
-
const s = JSON.parse(
|
|
24343
|
+
const s = JSON.parse(fs21.readFileSync(settingsPath, "utf8"));
|
|
23380
24344
|
entry = s.mcpServers?.["wairon"];
|
|
23381
24345
|
} catch {
|
|
23382
24346
|
return { mark: "error", note: "parse error" };
|
|
@@ -23384,7 +24348,7 @@ function mcpEntryHealth(settingsPath) {
|
|
|
23384
24348
|
if (!entry) return { mark: "warn", note: "not registered" };
|
|
23385
24349
|
if (entry.command === "node" && Array.isArray(entry.args) && typeof entry.args[0] === "string") {
|
|
23386
24350
|
const scriptPath = entry.args[0];
|
|
23387
|
-
if (!
|
|
24351
|
+
if (!fs21.existsSync(scriptPath)) {
|
|
23388
24352
|
return { mark: "error", note: `registered but the server path is missing \u2014 ${scriptPath}` };
|
|
23389
24353
|
}
|
|
23390
24354
|
}
|
|
@@ -23436,7 +24400,7 @@ async function runDoctor(options = {}) {
|
|
|
23436
24400
|
const { findChainingSubprojectsMissingConfig: findChainingSubprojectsMissingConfig2 } = (init_provision(), __toCommonJS(provision_exports));
|
|
23437
24401
|
const missing = findChainingSubprojectsMissingConfig2(getProjectRoot());
|
|
23438
24402
|
if (missing.length > 0) {
|
|
23439
|
-
line(tally, "warn", `${missing.length} chained subproject(s) have specs but no project.yaml (un-runnable standalone): ${missing.map((d) =>
|
|
24403
|
+
line(tally, "warn", `${missing.length} chained subproject(s) have specs but no project.yaml (un-runnable standalone): ${missing.map((d) => path30.relative(getProjectRoot(), d) || ".").join(", ")}. Run \`wairon doctor --fix\` to initialize them.`);
|
|
23440
24404
|
}
|
|
23441
24405
|
} catch {
|
|
23442
24406
|
}
|
|
@@ -23474,7 +24438,7 @@ async function runDoctor(options = {}) {
|
|
|
23474
24438
|
const gp = localGuideFilePath(process.cwd(), t);
|
|
23475
24439
|
if (!gp || seenGuides.has(gp)) continue;
|
|
23476
24440
|
seenGuides.add(gp);
|
|
23477
|
-
const rel2 =
|
|
24441
|
+
const rel2 = path30.relative(process.cwd(), gp).replace(/\\/g, "/");
|
|
23478
24442
|
if (!pathExists(gp)) {
|
|
23479
24443
|
line(tally, "warn", `${rel2} guide \u2014 not injected (run \`wairon generate\`)`);
|
|
23480
24444
|
continue;
|
|
@@ -23508,17 +24472,17 @@ async function runDoctor(options = {}) {
|
|
|
23508
24472
|
line(tally, h.mark, `Claude (project .mcp.json): ${h.note}${h.mark === "ok" ? "" : " \u2014 run `wairon mcp install --backend claude`"}`);
|
|
23509
24473
|
}
|
|
23510
24474
|
if (wantGemini) {
|
|
23511
|
-
const globalCfg =
|
|
24475
|
+
const globalCfg = path30.join(os9.homedir(), ".gemini", "antigravity-cli", "mcp_config.json");
|
|
23512
24476
|
const hg = mcpEntryHealth(globalCfg);
|
|
23513
24477
|
line(tally, hg.mark, `Antigravity (global mcp_config.json): ${hg.note}${hg.mark === "ok" ? "" : " \u2014 run `wairon mcp install --backend gemini --global`"}`);
|
|
23514
24478
|
const projPath = fromProjectRoot(".gemini", "settings.json");
|
|
23515
|
-
if (
|
|
24479
|
+
if (fs21.existsSync(projPath)) {
|
|
23516
24480
|
const hp = mcpEntryHealth(projPath);
|
|
23517
24481
|
line(tally, hp.mark === "error" ? "error" : "ok", `Gemini CLI (project): ${hp.note} ${import_chalk12.default.gray("(Antigravity ignores this file)")}`);
|
|
23518
24482
|
}
|
|
23519
24483
|
}
|
|
23520
|
-
const pluginDir =
|
|
23521
|
-
if (
|
|
24484
|
+
const pluginDir = path30.join(os9.homedir(), ".gemini", "config", "plugins", "wairon");
|
|
24485
|
+
if (fs21.existsSync(pluginDir)) {
|
|
23522
24486
|
line(tally, "warn", `Legacy Antigravity plugin present (${pluginDir}) \u2014 it collides with the wairon MCP server. Remove it with \`wairon doctor --fix\`.`);
|
|
23523
24487
|
}
|
|
23524
24488
|
logger.blank();
|
|
@@ -23549,7 +24513,7 @@ async function applyFixes() {
|
|
|
23549
24513
|
const legacySpecs = findLegacySpecFiles();
|
|
23550
24514
|
if (legacySpecs.length > 0) {
|
|
23551
24515
|
for (const { path: oldPath, expected: newPath } of legacySpecs) {
|
|
23552
|
-
|
|
24516
|
+
fs21.renameSync(oldPath, newPath);
|
|
23553
24517
|
}
|
|
23554
24518
|
console.log(` ${icon("ok")} Migrated ${legacySpecs.length} legacy spec file(s) to the new dot-prefixed unified schema.`);
|
|
23555
24519
|
}
|
|
@@ -23603,8 +24567,8 @@ function printSummary(tally) {
|
|
|
23603
24567
|
}
|
|
23604
24568
|
|
|
23605
24569
|
// src/commands/diagram.ts
|
|
23606
|
-
var
|
|
23607
|
-
var
|
|
24570
|
+
var fs22 = __toESM(require("fs"));
|
|
24571
|
+
var path31 = __toESM(require("path"));
|
|
23608
24572
|
init_logger();
|
|
23609
24573
|
init_loader();
|
|
23610
24574
|
init_fs();
|
|
@@ -23639,8 +24603,8 @@ function collectIssues() {
|
|
|
23639
24603
|
}
|
|
23640
24604
|
function writeCanvas(dest) {
|
|
23641
24605
|
const model = buildCanvasModel(collectIssues());
|
|
23642
|
-
ensureDir(
|
|
23643
|
-
|
|
24606
|
+
ensureDir(path31.dirname(path31.resolve(dest)));
|
|
24607
|
+
fs22.writeFileSync(dest, renderCanvasHtml(model), "utf-8");
|
|
23644
24608
|
}
|
|
23645
24609
|
function parseSequenceRef(ref) {
|
|
23646
24610
|
const sep6 = ref.includes(":") ? ref.lastIndexOf(":") : ref.lastIndexOf(".");
|
|
@@ -23655,55 +24619,55 @@ async function runDiagram(rawOptions = {}) {
|
|
|
23655
24619
|
assertProjectInitialized();
|
|
23656
24620
|
const options = applyFormat(rawOptions);
|
|
23657
24621
|
if (options.canvas && !options.all) {
|
|
23658
|
-
const dest2 = options.out ??
|
|
24622
|
+
const dest2 = options.out ?? path31.join(AI_PATHS.docsDir(), "diagrams", "canvas.html");
|
|
23659
24623
|
writeCanvas(dest2);
|
|
23660
24624
|
logger.success(`Interactive canvas written to ${dest2}`);
|
|
23661
24625
|
logger.info("Open it in a browser \u2014 fully self-contained (works offline).");
|
|
23662
24626
|
return;
|
|
23663
24627
|
}
|
|
23664
24628
|
if (options.drawio && !options.all) {
|
|
23665
|
-
const dest2 = options.out ??
|
|
23666
|
-
ensureDir(
|
|
23667
|
-
|
|
24629
|
+
const dest2 = options.out ?? path31.join(AI_PATHS.docsDir(), "diagrams", "architecture.drawio");
|
|
24630
|
+
ensureDir(path31.dirname(path31.resolve(dest2)));
|
|
24631
|
+
fs22.writeFileSync(dest2, generateDrawioXml(buildCanvasModel()), "utf-8");
|
|
23668
24632
|
logger.success(`draw.io diagram written to ${dest2}`);
|
|
23669
24633
|
logger.info("Open with draw.io / diagrams.net (or import into tools that accept the format).");
|
|
23670
24634
|
return;
|
|
23671
24635
|
}
|
|
23672
24636
|
if (options.excalidraw && !options.all) {
|
|
23673
|
-
const dest2 = options.out ??
|
|
23674
|
-
ensureDir(
|
|
23675
|
-
|
|
24637
|
+
const dest2 = options.out ?? path31.join(AI_PATHS.docsDir(), "diagrams", "architecture.excalidraw");
|
|
24638
|
+
ensureDir(path31.dirname(path31.resolve(dest2)));
|
|
24639
|
+
fs22.writeFileSync(dest2, generateExcalidrawScene(buildCanvasModel()), "utf-8");
|
|
23676
24640
|
logger.success(`Excalidraw scene written to ${dest2}`);
|
|
23677
24641
|
logger.info("Open with excalidraw.com or the VS Code extension.");
|
|
23678
24642
|
return;
|
|
23679
24643
|
}
|
|
23680
24644
|
const wantsMermaid = options.format?.toLowerCase().startsWith("mermaid") || !!options.subsystem || !!options.sequence;
|
|
23681
24645
|
if (!options.all && !options.sequence && !wantsMermaid) {
|
|
23682
|
-
const dest2 = options.out ??
|
|
24646
|
+
const dest2 = options.out ?? path31.join(AI_PATHS.docsDir(), "diagrams", "canvas.html");
|
|
23683
24647
|
writeCanvas(dest2);
|
|
23684
24648
|
logger.success(`Interactive canvas written to ${dest2}`);
|
|
23685
24649
|
logger.info("Open it in a browser \u2014 fully self-contained (works offline). Other formats: --format mermaid|drawio|excalidraw.");
|
|
23686
24650
|
return;
|
|
23687
24651
|
}
|
|
23688
24652
|
if (options.all) {
|
|
23689
|
-
const outDir = options.out ??
|
|
24653
|
+
const outDir = options.out ?? path31.join(AI_PATHS.docsDir(), "diagrams");
|
|
23690
24654
|
const files = generateDiagramSet();
|
|
23691
24655
|
if (files.length === 0) {
|
|
23692
24656
|
logger.warn("No diagrams to generate \u2014 the spec tree has no components yet.");
|
|
23693
24657
|
return;
|
|
23694
24658
|
}
|
|
23695
24659
|
for (const file of files) {
|
|
23696
|
-
const dest2 =
|
|
23697
|
-
ensureDir(
|
|
23698
|
-
|
|
24660
|
+
const dest2 = path31.join(outDir, file.relPath);
|
|
24661
|
+
ensureDir(path31.dirname(dest2));
|
|
24662
|
+
fs22.writeFileSync(dest2, toMarkdown(file), "utf-8");
|
|
23699
24663
|
}
|
|
23700
|
-
writeCanvas(
|
|
24664
|
+
writeCanvas(path31.join(outDir, "canvas.html"));
|
|
23701
24665
|
const exportModel = buildCanvasModel();
|
|
23702
|
-
|
|
23703
|
-
|
|
24666
|
+
fs22.writeFileSync(path31.join(outDir, "architecture.drawio"), generateDrawioXml(exportModel), "utf-8");
|
|
24667
|
+
fs22.writeFileSync(path31.join(outDir, "architecture.excalidraw"), generateExcalidrawScene(exportModel), "utf-8");
|
|
23704
24668
|
const graph = loadSpecGraph();
|
|
23705
|
-
const indexPath =
|
|
23706
|
-
|
|
24669
|
+
const indexPath = path31.join(outDir, "README.md");
|
|
24670
|
+
fs22.writeFileSync(indexPath, diagramSetIndex(files, graph.systemName), "utf-8");
|
|
23707
24671
|
logger.success(`Generated ${files.length} diagram(s) + interactive canvas.html + index into ${outDir}`);
|
|
23708
24672
|
for (const file of files.slice(0, 12)) {
|
|
23709
24673
|
logger.info(` ${file.relPath}`);
|
|
@@ -23714,26 +24678,26 @@ async function runDiagram(rawOptions = {}) {
|
|
|
23714
24678
|
let mermaid;
|
|
23715
24679
|
let title;
|
|
23716
24680
|
let defaultDest;
|
|
23717
|
-
const diagramsDir =
|
|
24681
|
+
const diagramsDir = path31.join(AI_PATHS.docsDir(), "diagrams");
|
|
23718
24682
|
if (options.sequence) {
|
|
23719
24683
|
const { component, method: method2 } = parseSequenceRef(options.sequence);
|
|
23720
24684
|
mermaid = generateSequenceDiagram(component, method2, { depth: options.depth });
|
|
23721
24685
|
title = `${component}.${method2} \u2014 narrative sequence`;
|
|
23722
|
-
defaultDest =
|
|
24686
|
+
defaultDest = path31.join(diagramsDir, "sequences", `${component.replace(/::/g, "--")}.${method2}.md`);
|
|
23723
24687
|
} else if (options.subsystem) {
|
|
23724
24688
|
mermaid = generateComponentDiagram({ subsystem: options.subsystem });
|
|
23725
24689
|
title = `${options.subsystem} \u2014 components`;
|
|
23726
|
-
defaultDest =
|
|
24690
|
+
defaultDest = path31.join(diagramsDir, "subsystems", `${options.subsystem.replace(/::/g, "--")}.md`);
|
|
23727
24691
|
} else {
|
|
23728
24692
|
mermaid = generateComponentDiagram();
|
|
23729
24693
|
title = "Component architecture";
|
|
23730
|
-
defaultDest =
|
|
24694
|
+
defaultDest = path31.join(diagramsDir, "system.md");
|
|
23731
24695
|
}
|
|
23732
24696
|
const dest = options.out ?? defaultDest;
|
|
23733
|
-
ensureDir(
|
|
24697
|
+
ensureDir(path31.dirname(path31.resolve(dest)));
|
|
23734
24698
|
const content = dest.endsWith(".mmd") ? `${mermaid}
|
|
23735
24699
|
` : toMarkdown({ relPath: dest, title, mermaid });
|
|
23736
|
-
|
|
24700
|
+
fs22.writeFileSync(dest, content, "utf-8");
|
|
23737
24701
|
logger.success(`Mermaid diagram written to ${dest}`);
|
|
23738
24702
|
logger.info("Renders on GitHub/IDE previews; use a .mmd --out path for raw Mermaid.");
|
|
23739
24703
|
}
|
|
@@ -23842,8 +24806,8 @@ Component variants (${variants.length})
|
|
|
23842
24806
|
}
|
|
23843
24807
|
|
|
23844
24808
|
// src/commands/packs.ts
|
|
23845
|
-
var
|
|
23846
|
-
var
|
|
24809
|
+
var fs23 = __toESM(require("fs"));
|
|
24810
|
+
var path32 = __toESM(require("path"));
|
|
23847
24811
|
var import_chalk16 = __toESM(require("chalk"));
|
|
23848
24812
|
var import_sdk = __toESM(require_dist());
|
|
23849
24813
|
init_logger();
|
|
@@ -23869,11 +24833,11 @@ function describe(probe2) {
|
|
|
23869
24833
|
return parts.join(", ");
|
|
23870
24834
|
}
|
|
23871
24835
|
function resolveSourceUnit(source) {
|
|
23872
|
-
const abs =
|
|
23873
|
-
if (!
|
|
24836
|
+
const abs = path32.resolve(source);
|
|
24837
|
+
if (!fs23.existsSync(abs)) {
|
|
23874
24838
|
throw new Error(`Pack source "${source}" does not exist.`);
|
|
23875
24839
|
}
|
|
23876
|
-
const isDir =
|
|
24840
|
+
const isDir = fs23.statSync(abs).isDirectory();
|
|
23877
24841
|
if (isDir && !packDirEntry(abs)) {
|
|
23878
24842
|
throw new Error(`"${source}" is a directory without a pack entry file (pack.yaml | pack.cjs | index.cjs | ...).`);
|
|
23879
24843
|
}
|
|
@@ -23889,7 +24853,7 @@ async function addPack(source, options = {}) {
|
|
|
23889
24853
|
}
|
|
23890
24854
|
const { abs } = resolveSourceUnit(source);
|
|
23891
24855
|
const scope = options.global ? "global" : "project";
|
|
23892
|
-
const probe2 = probePack(abs,
|
|
24856
|
+
const probe2 = probePack(abs, path32.dirname(abs), scope);
|
|
23893
24857
|
if (probe2.error) {
|
|
23894
24858
|
logger.error(probe2.error);
|
|
23895
24859
|
process.exitCode = 1;
|
|
@@ -23897,10 +24861,10 @@ async function addPack(source, options = {}) {
|
|
|
23897
24861
|
}
|
|
23898
24862
|
if (options.global) {
|
|
23899
24863
|
const destDir = globalPacksDir();
|
|
23900
|
-
const dest2 =
|
|
23901
|
-
if (
|
|
23902
|
-
|
|
23903
|
-
|
|
24864
|
+
const dest2 = path32.join(destDir, path32.basename(abs));
|
|
24865
|
+
if (path32.resolve(dest2) !== abs) {
|
|
24866
|
+
fs23.mkdirSync(destDir, { recursive: true });
|
|
24867
|
+
fs23.cpSync(abs, dest2, { recursive: true, force: true });
|
|
23904
24868
|
}
|
|
23905
24869
|
logger.success(`Installed pack "${probe2.name}" globally: ${dest2}`);
|
|
23906
24870
|
logger.info(`${describe(probe2)} \u2014 auto-loaded for every project on this machine (WAIRON_PACKS_DIR / ~/.wairon/packs).`);
|
|
@@ -23913,11 +24877,11 @@ async function addPack(source, options = {}) {
|
|
|
23913
24877
|
return;
|
|
23914
24878
|
}
|
|
23915
24879
|
const root = getProjectRoot();
|
|
23916
|
-
const relRef = `.wai/packs/${
|
|
23917
|
-
const dest =
|
|
23918
|
-
if (
|
|
23919
|
-
|
|
23920
|
-
|
|
24880
|
+
const relRef = `.wai/packs/${path32.basename(abs)}`;
|
|
24881
|
+
const dest = path32.join(root, ".wai", "packs", path32.basename(abs));
|
|
24882
|
+
if (path32.resolve(dest) !== abs) {
|
|
24883
|
+
fs23.mkdirSync(path32.dirname(dest), { recursive: true });
|
|
24884
|
+
fs23.cpSync(abs, dest, { recursive: true, force: true });
|
|
23921
24885
|
}
|
|
23922
24886
|
const config = loadProjectConfig();
|
|
23923
24887
|
const packs = config.extensions?.packs ?? [];
|
|
@@ -23926,14 +24890,14 @@ async function addPack(source, options = {}) {
|
|
|
23926
24890
|
saveProjectConfig(config);
|
|
23927
24891
|
logger.success(`Vendored pack "${probe2.name}" into ${relRef} and registered it in .wai/project.yaml.`);
|
|
23928
24892
|
} else {
|
|
23929
|
-
|
|
24893
|
+
fs23.cpSync(abs, dest, { recursive: true, force: true });
|
|
23930
24894
|
logger.success(`Pack "${probe2.name}" already registered \u2014 refreshed ${relRef} from the source.`);
|
|
23931
24895
|
}
|
|
23932
24896
|
logger.info(`${describe(probe2)} \u2014 commit .wai/ so CI and every clone enforce it.`);
|
|
23933
24897
|
}
|
|
23934
24898
|
async function addPackFromArchive(source, options) {
|
|
23935
|
-
const abs =
|
|
23936
|
-
if (!
|
|
24899
|
+
const abs = path32.resolve(source);
|
|
24900
|
+
if (!fs23.existsSync(abs) || !fs23.statSync(abs).isFile()) {
|
|
23937
24901
|
logger.error(`Pack archive "${source}" does not exist.`);
|
|
23938
24902
|
process.exitCode = 1;
|
|
23939
24903
|
return;
|
|
@@ -23948,27 +24912,27 @@ async function addPackFromArchive(source, options) {
|
|
|
23948
24912
|
process.exitCode = 1;
|
|
23949
24913
|
return;
|
|
23950
24914
|
}
|
|
23951
|
-
baseDir =
|
|
24915
|
+
baseDir = path32.join(getProjectRoot(), ".wai", "packs");
|
|
23952
24916
|
}
|
|
23953
|
-
const bytes =
|
|
23954
|
-
|
|
23955
|
-
const staging =
|
|
24917
|
+
const bytes = fs23.readFileSync(abs);
|
|
24918
|
+
fs23.mkdirSync(baseDir, { recursive: true });
|
|
24919
|
+
const staging = fs23.mkdtempSync(path32.join(baseDir, ".wpack-staging-"));
|
|
23956
24920
|
let result;
|
|
23957
24921
|
try {
|
|
23958
24922
|
result = (0, import_sdk.extractPack)(bytes, staging);
|
|
23959
24923
|
} catch (err) {
|
|
23960
|
-
|
|
23961
|
-
logger.error(`Failed to extract pack archive "${
|
|
24924
|
+
fs23.rmSync(staging, { recursive: true, force: true });
|
|
24925
|
+
logger.error(`Failed to extract pack archive "${path32.basename(abs)}": ${err instanceof Error ? err.message : String(err)}`);
|
|
23962
24926
|
process.exitCode = 1;
|
|
23963
24927
|
return;
|
|
23964
24928
|
}
|
|
23965
24929
|
const name = result.name;
|
|
23966
|
-
const destDir =
|
|
23967
|
-
if (
|
|
23968
|
-
|
|
23969
|
-
const probe2 = probePack(destDir,
|
|
24930
|
+
const destDir = path32.join(baseDir, name);
|
|
24931
|
+
if (fs23.existsSync(destDir)) fs23.rmSync(destDir, { recursive: true, force: true });
|
|
24932
|
+
fs23.renameSync(staging, destDir);
|
|
24933
|
+
const probe2 = probePack(destDir, path32.dirname(destDir), scope);
|
|
23970
24934
|
if (probe2.error) {
|
|
23971
|
-
|
|
24935
|
+
fs23.rmSync(destDir, { recursive: true, force: true });
|
|
23972
24936
|
logger.error(probe2.error);
|
|
23973
24937
|
process.exitCode = 1;
|
|
23974
24938
|
return;
|
|
@@ -23986,7 +24950,7 @@ async function addPackFromArchive(source, options) {
|
|
|
23986
24950
|
saveProjectConfig(config);
|
|
23987
24951
|
logger.success(`Installed pack "${probe2.name ?? name}" into ${relRef} and registered it in .wai/project.yaml.`);
|
|
23988
24952
|
} else {
|
|
23989
|
-
logger.success(`Pack "${probe2.name ?? name}" already registered \u2014 refreshed ${relRef} from ${
|
|
24953
|
+
logger.success(`Pack "${probe2.name ?? name}" already registered \u2014 refreshed ${relRef} from ${path32.basename(abs)}.`);
|
|
23990
24954
|
}
|
|
23991
24955
|
logger.info(`${describe(probe2)} \u2014 commit .wai/ so CI and every clone enforce it.`);
|
|
23992
24956
|
}
|
|
@@ -24007,7 +24971,7 @@ async function buildPack(source, options = {}) {
|
|
|
24007
24971
|
const sourceDir = source && source.length > 0 ? source : ".";
|
|
24008
24972
|
const result = (0, import_sdk.buildPack)(sourceDir);
|
|
24009
24973
|
const outPath = options.out ?? result.suggestedFileName;
|
|
24010
|
-
|
|
24974
|
+
fs23.writeFileSync(outPath, result.archive);
|
|
24011
24975
|
logger.success(`Built pack "${result.info.name}" v${result.info.version} \u2192 ${outPath} (${result.archive.byteLength} bytes)`);
|
|
24012
24976
|
logger.info(`Install it with \`wairon pack add ${outPath}\`, or upload it to a hosted instance.`);
|
|
24013
24977
|
}
|
|
@@ -24021,9 +24985,9 @@ async function listPacks() {
|
|
|
24021
24985
|
console.log(import_chalk16.default.bold.cyan(`\u25A0 Global (${globalPacksDir()})${useGlobal ? "" : import_chalk16.default.yellow(" [disabled: extensions.useGlobalPacks: false]")}`));
|
|
24022
24986
|
if (globalRefs.length === 0) console.log(import_chalk16.default.dim(" (none)"));
|
|
24023
24987
|
for (const ref of globalRefs) {
|
|
24024
|
-
const probe2 = probePack(ref,
|
|
24025
|
-
if (probe2.error) console.log(` ${import_chalk16.default.red("\u2716")} ${
|
|
24026
|
-
else console.log(` ${import_chalk16.default.green("\u25CF")} ${import_chalk16.default.bold(probe2.name ??
|
|
24988
|
+
const probe2 = probePack(ref, path32.dirname(ref), "global");
|
|
24989
|
+
if (probe2.error) console.log(` ${import_chalk16.default.red("\u2716")} ${path32.basename(ref)} \u2014 ${import_chalk16.default.red(probe2.error)}`);
|
|
24990
|
+
else console.log(` ${import_chalk16.default.green("\u25CF")} ${import_chalk16.default.bold(probe2.name ?? path32.basename(ref))} ${import_chalk16.default.dim(describe(probe2))}`);
|
|
24027
24991
|
}
|
|
24028
24992
|
console.log("");
|
|
24029
24993
|
if (!inProject) {
|
|
@@ -24044,9 +25008,9 @@ async function listPacks() {
|
|
|
24044
25008
|
async function removePack(name, options = {}) {
|
|
24045
25009
|
if (options.global) {
|
|
24046
25010
|
for (const ref of discoverPacks(globalPacksDir())) {
|
|
24047
|
-
const probe2 = probePack(ref,
|
|
24048
|
-
if (probe2.name === name ||
|
|
24049
|
-
|
|
25011
|
+
const probe2 = probePack(ref, path32.dirname(ref), "global");
|
|
25012
|
+
if (probe2.name === name || path32.basename(ref) === name) {
|
|
25013
|
+
fs23.rmSync(ref, { recursive: true, force: true });
|
|
24050
25014
|
logger.success(`Removed global pack "${probe2.name ?? name}" (${ref}).`);
|
|
24051
25015
|
return;
|
|
24052
25016
|
}
|
|
@@ -24065,16 +25029,16 @@ async function removePack(name, options = {}) {
|
|
|
24065
25029
|
const packs = config.extensions?.packs ?? [];
|
|
24066
25030
|
for (const ref of packs) {
|
|
24067
25031
|
const probe2 = probePack(ref, root, "project");
|
|
24068
|
-
if (probe2.name === name || ref === name ||
|
|
25032
|
+
if (probe2.name === name || ref === name || path32.basename(ref) === name) {
|
|
24069
25033
|
config.extensions = {
|
|
24070
25034
|
packs: packs.filter((p) => p !== ref),
|
|
24071
25035
|
useGlobalPacks: config.extensions?.useGlobalPacks ?? true
|
|
24072
25036
|
};
|
|
24073
25037
|
saveProjectConfig(config);
|
|
24074
|
-
const resolved =
|
|
24075
|
-
const vendorDir =
|
|
24076
|
-
if (resolved.startsWith(vendorDir +
|
|
24077
|
-
|
|
25038
|
+
const resolved = path32.resolve(root, ref);
|
|
25039
|
+
const vendorDir = path32.resolve(root, ".wai", "packs");
|
|
25040
|
+
if (resolved.startsWith(vendorDir + path32.sep)) {
|
|
25041
|
+
fs23.rmSync(resolved, { recursive: true, force: true });
|
|
24078
25042
|
logger.success(`Deregistered pack "${probe2.name ?? name}" and deleted ${ref}.`);
|
|
24079
25043
|
} else {
|
|
24080
25044
|
logger.success(`Deregistered pack "${probe2.name ?? name}" (files at ${ref} left in place).`);
|
|
@@ -24088,8 +25052,8 @@ async function removePack(name, options = {}) {
|
|
|
24088
25052
|
|
|
24089
25053
|
// src/commands/host.ts
|
|
24090
25054
|
var fs50 = __toESM(require("fs"));
|
|
24091
|
-
var
|
|
24092
|
-
var
|
|
25055
|
+
var path59 = __toESM(require("path"));
|
|
25056
|
+
var os10 = __toESM(require("os"));
|
|
24093
25057
|
var crypto20 = __toESM(require("crypto"));
|
|
24094
25058
|
var import_child_process5 = require("child_process");
|
|
24095
25059
|
var import_chalk17 = __toESM(require("chalk"));
|
|
@@ -24116,29 +25080,29 @@ var UNAUTHENTICATED = {
|
|
|
24116
25080
|
var WEB_SESSION_PREFIX = "ws_";
|
|
24117
25081
|
|
|
24118
25082
|
// src/server/credentials.ts
|
|
24119
|
-
var
|
|
24120
|
-
var
|
|
25083
|
+
var fs24 = __toESM(require("fs"));
|
|
25084
|
+
var path33 = __toESM(require("path"));
|
|
24121
25085
|
var crypto3 = __toESM(require("crypto"));
|
|
24122
25086
|
var HASH_NS = "wairon:token:v1";
|
|
24123
25087
|
function hashToken(token) {
|
|
24124
25088
|
return crypto3.createHash("sha256").update(`${HASH_NS}:${token}`).digest("hex");
|
|
24125
25089
|
}
|
|
24126
25090
|
function storePath(dataDir) {
|
|
24127
|
-
return
|
|
25091
|
+
return path33.join(dataDir, "auth", "credentials.json");
|
|
24128
25092
|
}
|
|
24129
25093
|
function load3(dataDir) {
|
|
24130
25094
|
try {
|
|
24131
|
-
return JSON.parse(
|
|
25095
|
+
return JSON.parse(fs24.readFileSync(storePath(dataDir), "utf8"));
|
|
24132
25096
|
} catch {
|
|
24133
25097
|
return [];
|
|
24134
25098
|
}
|
|
24135
25099
|
}
|
|
24136
25100
|
function save(dataDir, records) {
|
|
24137
25101
|
const p = storePath(dataDir);
|
|
24138
|
-
|
|
25102
|
+
fs24.mkdirSync(path33.dirname(p), { recursive: true });
|
|
24139
25103
|
const tmp = `${p}.tmp`;
|
|
24140
|
-
|
|
24141
|
-
|
|
25104
|
+
fs24.writeFileSync(tmp, JSON.stringify(records, null, 2) + "\n");
|
|
25105
|
+
fs24.renameSync(tmp, p);
|
|
24142
25106
|
}
|
|
24143
25107
|
function digestEquals(a, b) {
|
|
24144
25108
|
const ab = Buffer.from(a, "hex");
|
|
@@ -24183,17 +25147,17 @@ function listByOwner(dataDir, ownerUserId) {
|
|
|
24183
25147
|
}
|
|
24184
25148
|
|
|
24185
25149
|
// src/server/websessions.ts
|
|
24186
|
-
var
|
|
24187
|
-
var
|
|
25150
|
+
var fs25 = __toESM(require("fs"));
|
|
25151
|
+
var path34 = __toESM(require("path"));
|
|
24188
25152
|
var crypto4 = __toESM(require("crypto"));
|
|
24189
25153
|
function storePath2(dataDir) {
|
|
24190
|
-
return
|
|
25154
|
+
return path34.join(dataDir, "web-sessions.json");
|
|
24191
25155
|
}
|
|
24192
25156
|
function readSessions(dataDir) {
|
|
24193
25157
|
const p = storePath2(dataDir);
|
|
24194
25158
|
let raw;
|
|
24195
25159
|
try {
|
|
24196
|
-
raw =
|
|
25160
|
+
raw = fs25.readFileSync(p, "utf8");
|
|
24197
25161
|
} catch (e) {
|
|
24198
25162
|
if (e.code === "ENOENT") return [];
|
|
24199
25163
|
throw new Error(`Failed to read web session store at ${p}: ${e.message}`);
|
|
@@ -24208,10 +25172,10 @@ function readSessions(dataDir) {
|
|
|
24208
25172
|
}
|
|
24209
25173
|
function persistSessions(dataDir, sessions) {
|
|
24210
25174
|
const p = storePath2(dataDir);
|
|
24211
|
-
|
|
25175
|
+
fs25.mkdirSync(path34.dirname(p), { recursive: true });
|
|
24212
25176
|
const tmp = `${p}.tmp`;
|
|
24213
|
-
|
|
24214
|
-
|
|
25177
|
+
fs25.writeFileSync(tmp, JSON.stringify(sessions, null, 2) + "\n");
|
|
25178
|
+
fs25.renameSync(tmp, p);
|
|
24215
25179
|
}
|
|
24216
25180
|
function mintSessionId() {
|
|
24217
25181
|
return `${WEB_SESSION_PREFIX}${crypto4.randomBytes(24).toString("hex")}`;
|
|
@@ -24372,17 +25336,17 @@ function listWebSessionsBySubject(dataDir, userId) {
|
|
|
24372
25336
|
}
|
|
24373
25337
|
|
|
24374
25338
|
// src/server/users.ts
|
|
24375
|
-
var
|
|
24376
|
-
var
|
|
25339
|
+
var fs26 = __toESM(require("fs"));
|
|
25340
|
+
var path35 = __toESM(require("path"));
|
|
24377
25341
|
var VALID_STATUSES = ["active", "inactive", "suspended", "deactivated", "disabled"];
|
|
24378
25342
|
function storePath3(dataDir) {
|
|
24379
|
-
return
|
|
25343
|
+
return path35.join(dataDir, "users.json");
|
|
24380
25344
|
}
|
|
24381
25345
|
function loadStore(dataDir) {
|
|
24382
25346
|
const p = storePath3(dataDir);
|
|
24383
25347
|
let raw;
|
|
24384
25348
|
try {
|
|
24385
|
-
raw =
|
|
25349
|
+
raw = fs26.readFileSync(p, "utf8");
|
|
24386
25350
|
} catch (err) {
|
|
24387
25351
|
if (err.code === "ENOENT") return [];
|
|
24388
25352
|
throw new Error(`Cannot read hosted-user store at ${p}: ${err.message}`);
|
|
@@ -24400,10 +25364,10 @@ function loadStore(dataDir) {
|
|
|
24400
25364
|
}
|
|
24401
25365
|
function replaceAll(dataDir, records) {
|
|
24402
25366
|
const p = storePath3(dataDir);
|
|
24403
|
-
|
|
25367
|
+
fs26.mkdirSync(path35.dirname(p), { recursive: true });
|
|
24404
25368
|
const tmp = `${p}.tmp`;
|
|
24405
|
-
|
|
24406
|
-
|
|
25369
|
+
fs26.writeFileSync(tmp, JSON.stringify(records, null, 2) + "\n");
|
|
25370
|
+
fs26.renameSync(tmp, p);
|
|
24407
25371
|
}
|
|
24408
25372
|
function registryUpsert(dataDir, record2) {
|
|
24409
25373
|
const records = loadStore(dataDir);
|
|
@@ -24505,11 +25469,11 @@ function remapUnitReferences(dataDir, remap, removedScopeIds) {
|
|
|
24505
25469
|
}
|
|
24506
25470
|
|
|
24507
25471
|
// src/server/instance.ts
|
|
24508
|
-
var
|
|
24509
|
-
var
|
|
25472
|
+
var fs27 = __toESM(require("fs"));
|
|
25473
|
+
var path36 = __toESM(require("path"));
|
|
24510
25474
|
var import_crypto = require("crypto");
|
|
24511
25475
|
function storePath4(dataDir) {
|
|
24512
|
-
return
|
|
25476
|
+
return path36.join(dataDir, "instance.json");
|
|
24513
25477
|
}
|
|
24514
25478
|
var InstanceIdentityStore = class {
|
|
24515
25479
|
constructor(dataDir) {
|
|
@@ -24526,7 +25490,7 @@ var InstanceIdentityStore = class {
|
|
|
24526
25490
|
const p = storePath4(this.dataDir);
|
|
24527
25491
|
let raw;
|
|
24528
25492
|
try {
|
|
24529
|
-
raw =
|
|
25493
|
+
raw = fs27.readFileSync(p, "utf8");
|
|
24530
25494
|
} catch (err) {
|
|
24531
25495
|
if (err.code === "ENOENT") return null;
|
|
24532
25496
|
throw new Error(`Cannot read instance identity at ${p}: ${err.message}`);
|
|
@@ -24549,10 +25513,10 @@ var InstanceIdentityStore = class {
|
|
|
24549
25513
|
* never truncates the file. Only called by the registry's create-once seed. */
|
|
24550
25514
|
replace(identity) {
|
|
24551
25515
|
const p = storePath4(this.dataDir);
|
|
24552
|
-
|
|
25516
|
+
fs27.mkdirSync(path36.dirname(p), { recursive: true });
|
|
24553
25517
|
const tmp = `${p}.tmp`;
|
|
24554
|
-
|
|
24555
|
-
|
|
25518
|
+
fs27.writeFileSync(tmp, JSON.stringify(identity, null, 2) + "\n");
|
|
25519
|
+
fs27.renameSync(tmp, p);
|
|
24556
25520
|
}
|
|
24557
25521
|
};
|
|
24558
25522
|
var InstanceIdentityRegistry = class {
|
|
@@ -24608,8 +25572,8 @@ function getInstanceIdentity(dataDir) {
|
|
|
24608
25572
|
}
|
|
24609
25573
|
|
|
24610
25574
|
// src/utils/secrets.ts
|
|
24611
|
-
var
|
|
24612
|
-
var
|
|
25575
|
+
var fs28 = __toESM(require("fs"));
|
|
25576
|
+
var path37 = __toESM(require("path"));
|
|
24613
25577
|
var ENV_FALLBACK = {
|
|
24614
25578
|
"git-token": ["WAIRON_GIT_TOKEN"],
|
|
24615
25579
|
"notion-token": ["WAIRON_NOTION_TOKEN"],
|
|
@@ -24618,13 +25582,13 @@ var ENV_FALLBACK = {
|
|
|
24618
25582
|
};
|
|
24619
25583
|
function storePath5() {
|
|
24620
25584
|
const dataDir = process.env["WAIRON_DATA_DIR"];
|
|
24621
|
-
return dataDir ?
|
|
25585
|
+
return dataDir ? path37.join(dataDir, "auth", "secrets.json") : null;
|
|
24622
25586
|
}
|
|
24623
25587
|
function readStore() {
|
|
24624
25588
|
const p = storePath5();
|
|
24625
25589
|
if (!p) return {};
|
|
24626
25590
|
try {
|
|
24627
|
-
return JSON.parse(
|
|
25591
|
+
return JSON.parse(fs28.readFileSync(p, "utf8"));
|
|
24628
25592
|
} catch {
|
|
24629
25593
|
return {};
|
|
24630
25594
|
}
|
|
@@ -24649,10 +25613,10 @@ function setSecret(key, value) {
|
|
|
24649
25613
|
if (!p) throw new Error("WAIRON_DATA_DIR is not set \u2014 a running server needs it to store secrets.");
|
|
24650
25614
|
const store = readStore();
|
|
24651
25615
|
store[key] = value;
|
|
24652
|
-
|
|
25616
|
+
fs28.mkdirSync(path37.dirname(p), { recursive: true });
|
|
24653
25617
|
const tmp = `${p}.tmp`;
|
|
24654
|
-
|
|
24655
|
-
|
|
25618
|
+
fs28.writeFileSync(tmp, JSON.stringify(store, null, 2) + "\n");
|
|
25619
|
+
fs28.renameSync(tmp, p);
|
|
24656
25620
|
}
|
|
24657
25621
|
function listSecretKeys() {
|
|
24658
25622
|
return Object.keys(readStore());
|
|
@@ -24860,17 +25824,17 @@ function verifySsoState(state) {
|
|
|
24860
25824
|
}
|
|
24861
25825
|
|
|
24862
25826
|
// src/server/organization.ts
|
|
24863
|
-
var
|
|
24864
|
-
var
|
|
25827
|
+
var fs29 = __toESM(require("fs"));
|
|
25828
|
+
var path38 = __toESM(require("path"));
|
|
24865
25829
|
var crypto6 = __toESM(require("crypto"));
|
|
24866
25830
|
function storePath6(dataDir) {
|
|
24867
|
-
return
|
|
25831
|
+
return path38.join(dataDir, "organization.json");
|
|
24868
25832
|
}
|
|
24869
25833
|
function readState(dataDir) {
|
|
24870
25834
|
const p = storePath6(dataDir);
|
|
24871
25835
|
let raw;
|
|
24872
25836
|
try {
|
|
24873
|
-
raw =
|
|
25837
|
+
raw = fs29.readFileSync(p, "utf8");
|
|
24874
25838
|
} catch (e) {
|
|
24875
25839
|
if (e.code === "ENOENT") return { units: [], placements: [] };
|
|
24876
25840
|
throw new Error(`Failed to read organization store at ${p}: ${e.message}`);
|
|
@@ -24887,10 +25851,10 @@ function readState(dataDir) {
|
|
|
24887
25851
|
}
|
|
24888
25852
|
function persistState(dataDir, state) {
|
|
24889
25853
|
const p = storePath6(dataDir);
|
|
24890
|
-
|
|
25854
|
+
fs29.mkdirSync(path38.dirname(p), { recursive: true });
|
|
24891
25855
|
const tmp = `${p}.tmp`;
|
|
24892
|
-
|
|
24893
|
-
|
|
25856
|
+
fs29.writeFileSync(tmp, JSON.stringify(state, null, 2) + "\n");
|
|
25857
|
+
fs29.renameSync(tmp, p);
|
|
24894
25858
|
}
|
|
24895
25859
|
var SLUG_PATTERN = /^[a-z0-9-]+$/;
|
|
24896
25860
|
var UNIT_KINDS = ["business_entity", "department", "team", "group"];
|
|
@@ -25259,11 +26223,11 @@ function getOrganizationUnit(dataDir, id) {
|
|
|
25259
26223
|
}
|
|
25260
26224
|
|
|
25261
26225
|
// src/server/permissions.ts
|
|
25262
|
-
var
|
|
25263
|
-
var
|
|
26226
|
+
var fs30 = __toESM(require("fs"));
|
|
26227
|
+
var path39 = __toESM(require("path"));
|
|
25264
26228
|
var import_crypto2 = require("crypto");
|
|
25265
26229
|
function storePath7(dataDir) {
|
|
25266
|
-
return
|
|
26230
|
+
return path39.join(dataDir, "permissions.json");
|
|
25267
26231
|
}
|
|
25268
26232
|
function assignmentKey(a) {
|
|
25269
26233
|
return [a.subjectKind, a.subjectId ?? "", a.scopeKind, a.scopeId ?? "", a.capability].join("|");
|
|
@@ -25272,7 +26236,7 @@ function load4(dataDir) {
|
|
|
25272
26236
|
const p = storePath7(dataDir);
|
|
25273
26237
|
let raw;
|
|
25274
26238
|
try {
|
|
25275
|
-
raw =
|
|
26239
|
+
raw = fs30.readFileSync(p, "utf8");
|
|
25276
26240
|
} catch (err) {
|
|
25277
26241
|
if (err.code === "ENOENT") return [];
|
|
25278
26242
|
throw new Error(`Cannot read permission store at ${p}: ${err.message}`);
|
|
@@ -25290,10 +26254,10 @@ function load4(dataDir) {
|
|
|
25290
26254
|
}
|
|
25291
26255
|
function replaceAll2(dataDir, assignments) {
|
|
25292
26256
|
const p = storePath7(dataDir);
|
|
25293
|
-
|
|
26257
|
+
fs30.mkdirSync(path39.dirname(p), { recursive: true });
|
|
25294
26258
|
const tmp = `${p}.tmp`;
|
|
25295
|
-
|
|
25296
|
-
|
|
26259
|
+
fs30.writeFileSync(tmp, JSON.stringify(assignments, null, 2) + "\n");
|
|
26260
|
+
fs30.renameSync(tmp, p);
|
|
25297
26261
|
}
|
|
25298
26262
|
function registrySet(dataDir, assignment) {
|
|
25299
26263
|
const assignments = load4(dataDir);
|
|
@@ -25379,8 +26343,8 @@ function getAssignment(dataDir, assignmentId) {
|
|
|
25379
26343
|
}
|
|
25380
26344
|
|
|
25381
26345
|
// src/server/roles.ts
|
|
25382
|
-
var
|
|
25383
|
-
var
|
|
26346
|
+
var fs31 = __toESM(require("fs"));
|
|
26347
|
+
var path40 = __toESM(require("path"));
|
|
25384
26348
|
var BUILTIN_ROLES = [
|
|
25385
26349
|
{
|
|
25386
26350
|
id: SSO_ADMIN_ROLE_ID,
|
|
@@ -25398,13 +26362,13 @@ function isBuiltinRoleId(roleId) {
|
|
|
25398
26362
|
return BUILTIN_ROLE_IDS.has(roleId);
|
|
25399
26363
|
}
|
|
25400
26364
|
function storePath8(dataDir) {
|
|
25401
|
-
return
|
|
26365
|
+
return path40.join(dataDir, "roles.json");
|
|
25402
26366
|
}
|
|
25403
26367
|
function load5(dataDir) {
|
|
25404
26368
|
const p = storePath8(dataDir);
|
|
25405
26369
|
let raw;
|
|
25406
26370
|
try {
|
|
25407
|
-
raw =
|
|
26371
|
+
raw = fs31.readFileSync(p, "utf8");
|
|
25408
26372
|
} catch (err) {
|
|
25409
26373
|
if (err.code === "ENOENT") return [];
|
|
25410
26374
|
throw new Error(`Cannot read role store at ${p}: ${err.message}`);
|
|
@@ -25422,10 +26386,10 @@ function load5(dataDir) {
|
|
|
25422
26386
|
}
|
|
25423
26387
|
function replaceAll3(dataDir, roles) {
|
|
25424
26388
|
const p = storePath8(dataDir);
|
|
25425
|
-
|
|
26389
|
+
fs31.mkdirSync(path40.dirname(p), { recursive: true });
|
|
25426
26390
|
const tmp = `${p}.tmp`;
|
|
25427
|
-
|
|
25428
|
-
|
|
26391
|
+
fs31.writeFileSync(tmp, JSON.stringify(roles, null, 2) + "\n");
|
|
26392
|
+
fs31.renameSync(tmp, p);
|
|
25429
26393
|
}
|
|
25430
26394
|
function registryCreate(dataDir, role) {
|
|
25431
26395
|
if (isBuiltinRoleId(role.id)) {
|
|
@@ -25659,131 +26623,14 @@ function actionableUnitIds(scopes) {
|
|
|
25659
26623
|
}
|
|
25660
26624
|
|
|
25661
26625
|
// src/server/projects.ts
|
|
25662
|
-
var
|
|
25663
|
-
var
|
|
25664
|
-
|
|
25665
|
-
|
|
25666
|
-
return typeof id === "string" && ID_RE.test(id);
|
|
25667
|
-
}
|
|
25668
|
-
function registryPath(dataDir) {
|
|
25669
|
-
return path40.join(dataDir, "projects.json");
|
|
25670
|
-
}
|
|
25671
|
-
function load6(dataDir) {
|
|
25672
|
-
try {
|
|
25673
|
-
return JSON.parse(fs31.readFileSync(registryPath(dataDir), "utf8"));
|
|
25674
|
-
} catch {
|
|
25675
|
-
return [];
|
|
25676
|
-
}
|
|
25677
|
-
}
|
|
25678
|
-
function save2(dataDir, records) {
|
|
25679
|
-
const p = registryPath(dataDir);
|
|
25680
|
-
fs31.mkdirSync(path40.dirname(p), { recursive: true });
|
|
25681
|
-
const tmp = `${p}.tmp`;
|
|
25682
|
-
fs31.writeFileSync(tmp, JSON.stringify(records, null, 2) + "\n");
|
|
25683
|
-
fs31.renameSync(tmp, p);
|
|
25684
|
-
}
|
|
25685
|
-
function projectRoot(dataDir, id) {
|
|
25686
|
-
return path40.join(dataDir, "projects", id);
|
|
25687
|
-
}
|
|
25688
|
-
function existingProjectRoot(dataDir, id) {
|
|
25689
|
-
if (!isValidProjectId(id)) return null;
|
|
25690
|
-
const rec = load6(dataDir).find((r) => r.id === id);
|
|
25691
|
-
return rec ? rec.rootPath : null;
|
|
25692
|
-
}
|
|
25693
|
-
function createProjectRecord(dataDir, id) {
|
|
25694
|
-
if (!isValidProjectId(id)) {
|
|
25695
|
-
throw new Error(`Invalid project id "${id}" (allowed: lowercase letters, digits, hyphen).`);
|
|
25696
|
-
}
|
|
25697
|
-
const records = load6(dataDir);
|
|
25698
|
-
if (records.some((r) => r.id === id)) {
|
|
25699
|
-
throw new Error(`Project "${id}" already exists.`);
|
|
25700
|
-
}
|
|
25701
|
-
const root = projectRoot(dataDir, id);
|
|
25702
|
-
fs31.mkdirSync(root, { recursive: true });
|
|
25703
|
-
const record2 = {
|
|
25704
|
-
id,
|
|
25705
|
-
rootPath: root,
|
|
25706
|
-
status: "active",
|
|
25707
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
25708
|
-
};
|
|
25709
|
-
records.push(record2);
|
|
25710
|
-
save2(dataDir, records);
|
|
25711
|
-
return record2;
|
|
25712
|
-
}
|
|
25713
|
-
function registerLocalDevProject(dataDir, id, rootPath) {
|
|
25714
|
-
if (!isValidProjectId(id)) {
|
|
25715
|
-
throw new Error(`Invalid project id "${id}" (allowed: lowercase letters, digits, hyphen).`);
|
|
25716
|
-
}
|
|
25717
|
-
const records = load6(dataDir);
|
|
25718
|
-
const existing = records.find((r) => r.id === id);
|
|
25719
|
-
const record2 = {
|
|
25720
|
-
id,
|
|
25721
|
-
rootPath,
|
|
25722
|
-
status: "active",
|
|
25723
|
-
createdAt: existing?.createdAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
25724
|
-
};
|
|
25725
|
-
const next = existing ? records.map((r) => r.id === id ? record2 : r) : [...records, record2];
|
|
25726
|
-
save2(dataDir, next);
|
|
25727
|
-
return record2;
|
|
25728
|
-
}
|
|
25729
|
-
function listProjectRecords(dataDir) {
|
|
25730
|
-
return load6(dataDir);
|
|
25731
|
-
}
|
|
25732
|
-
function removeProjectRecord(dataDir, id) {
|
|
25733
|
-
const records = load6(dataDir);
|
|
25734
|
-
const rec = records.find((r) => r.id === id);
|
|
25735
|
-
if (rec) {
|
|
25736
|
-
try {
|
|
25737
|
-
fs31.rmSync(rec.rootPath, { recursive: true, force: true });
|
|
25738
|
-
} catch {
|
|
25739
|
-
}
|
|
25740
|
-
}
|
|
25741
|
-
save2(dataDir, records.filter((r) => r.id !== id));
|
|
25742
|
-
}
|
|
25743
|
-
function resolveProjectRoot(dataDir, principal, selector) {
|
|
25744
|
-
const authorized = principal.projects;
|
|
25745
|
-
const wildcard = authorized.includes("*");
|
|
25746
|
-
let target;
|
|
25747
|
-
if (selector) {
|
|
25748
|
-
if (!wildcard && !authorized.includes(selector)) return null;
|
|
25749
|
-
target = selector;
|
|
25750
|
-
} else if (!wildcard && authorized.length === 1) {
|
|
25751
|
-
target = authorized[0];
|
|
25752
|
-
} else {
|
|
25753
|
-
return null;
|
|
25754
|
-
}
|
|
25755
|
-
if (!isValidProjectId(target)) return null;
|
|
25756
|
-
const rec = load6(dataDir).find((r) => r.id === target);
|
|
25757
|
-
if (!rec || rec.status !== "active") return null;
|
|
25758
|
-
return rec.rootPath;
|
|
25759
|
-
}
|
|
25760
|
-
|
|
25761
|
-
// src/server/adapters.ts
|
|
25762
|
-
init_statehash();
|
|
25763
|
-
|
|
25764
|
-
// src/core/lockfile.ts
|
|
25765
|
-
var fs32 = __toESM(require("fs"));
|
|
25766
|
-
var path41 = __toESM(require("path"));
|
|
26626
|
+
var fs35 = __toESM(require("fs"));
|
|
26627
|
+
var path44 = __toESM(require("path"));
|
|
26628
|
+
init_loader();
|
|
26629
|
+
init_yaml();
|
|
25767
26630
|
init_fs();
|
|
25768
|
-
function lockPath() {
|
|
25769
|
-
return aiDir("lock.json");
|
|
25770
|
-
}
|
|
25771
|
-
function readLockRecord() {
|
|
25772
|
-
try {
|
|
25773
|
-
return JSON.parse(fs32.readFileSync(lockPath(), "utf8"));
|
|
25774
|
-
} catch {
|
|
25775
|
-
return null;
|
|
25776
|
-
}
|
|
25777
|
-
}
|
|
25778
|
-
function writeLockRecord(record2) {
|
|
25779
|
-
const p = lockPath();
|
|
25780
|
-
fs32.mkdirSync(path41.dirname(p), { recursive: true });
|
|
25781
|
-
const tmp = `${p}.tmp`;
|
|
25782
|
-
fs32.writeFileSync(tmp, JSON.stringify(record2, null, 2) + "\n");
|
|
25783
|
-
fs32.renameSync(tmp, p);
|
|
25784
|
-
}
|
|
25785
26631
|
|
|
25786
26632
|
// src/server/adapters.ts
|
|
26633
|
+
init_statehash();
|
|
25787
26634
|
init_specs2();
|
|
25788
26635
|
init_provision();
|
|
25789
26636
|
init_validation();
|
|
@@ -25794,37 +26641,37 @@ init_types();
|
|
|
25794
26641
|
init_server();
|
|
25795
26642
|
|
|
25796
26643
|
// src/git/config.ts
|
|
25797
|
-
var
|
|
25798
|
-
var
|
|
26644
|
+
var fs32 = __toESM(require("fs"));
|
|
26645
|
+
var path41 = __toESM(require("path"));
|
|
25799
26646
|
init_fs();
|
|
25800
26647
|
function configPath() {
|
|
25801
26648
|
return aiDir("git.json");
|
|
25802
26649
|
}
|
|
25803
26650
|
function readGitConfig() {
|
|
25804
26651
|
try {
|
|
25805
|
-
return JSON.parse(
|
|
26652
|
+
return JSON.parse(fs32.readFileSync(configPath(), "utf8"));
|
|
25806
26653
|
} catch {
|
|
25807
26654
|
return null;
|
|
25808
26655
|
}
|
|
25809
26656
|
}
|
|
25810
26657
|
function writeGitConfig(config) {
|
|
25811
26658
|
const p = configPath();
|
|
25812
|
-
|
|
26659
|
+
fs32.mkdirSync(path41.dirname(p), { recursive: true });
|
|
25813
26660
|
const tmp = `${p}.tmp`;
|
|
25814
|
-
|
|
25815
|
-
|
|
26661
|
+
fs32.writeFileSync(tmp, JSON.stringify(config, null, 2) + "\n");
|
|
26662
|
+
fs32.renameSync(tmp, p);
|
|
25816
26663
|
}
|
|
25817
26664
|
function clearGitConfig() {
|
|
25818
26665
|
try {
|
|
25819
|
-
|
|
26666
|
+
fs32.rmSync(configPath(), { force: true });
|
|
25820
26667
|
} catch {
|
|
25821
26668
|
}
|
|
25822
26669
|
}
|
|
25823
26670
|
|
|
25824
26671
|
// src/git/adapter.ts
|
|
25825
26672
|
var import_child_process3 = require("child_process");
|
|
25826
|
-
var
|
|
25827
|
-
var
|
|
26673
|
+
var fs33 = __toESM(require("fs"));
|
|
26674
|
+
var path42 = __toESM(require("path"));
|
|
25828
26675
|
init_fs();
|
|
25829
26676
|
function git(args, cwd) {
|
|
25830
26677
|
return (0, import_child_process3.execFileSync)("git", args, {
|
|
@@ -25876,10 +26723,10 @@ function compareUrl(remote, defaultBranch, workingBranch) {
|
|
|
25876
26723
|
return `${web}/compare/${encodeURIComponent(defaultBranch)}...${encodeURIComponent(workingBranch)}`;
|
|
25877
26724
|
}
|
|
25878
26725
|
function excludeLocalFiles() {
|
|
25879
|
-
const excludePath =
|
|
26726
|
+
const excludePath = path42.join(getProjectRoot(), ".git", "info", "exclude");
|
|
25880
26727
|
try {
|
|
25881
|
-
|
|
25882
|
-
|
|
26728
|
+
fs33.mkdirSync(path42.dirname(excludePath), { recursive: true });
|
|
26729
|
+
fs33.appendFileSync(excludePath, "\n.wai/lock.json\n.wai/git.json\n");
|
|
25883
26730
|
} catch {
|
|
25884
26731
|
}
|
|
25885
26732
|
}
|
|
@@ -25944,39 +26791,39 @@ function configureSync(periodicSyncMinutes, skipIfClean) {
|
|
|
25944
26791
|
}
|
|
25945
26792
|
|
|
25946
26793
|
// src/producers/config.ts
|
|
25947
|
-
var
|
|
25948
|
-
var
|
|
26794
|
+
var fs34 = __toESM(require("fs"));
|
|
26795
|
+
var path43 = __toESM(require("path"));
|
|
25949
26796
|
init_fs();
|
|
25950
26797
|
function configPath2() {
|
|
25951
26798
|
return aiDir("producers.json");
|
|
25952
26799
|
}
|
|
25953
|
-
function
|
|
26800
|
+
function load6() {
|
|
25954
26801
|
try {
|
|
25955
|
-
return JSON.parse(
|
|
26802
|
+
return JSON.parse(fs34.readFileSync(configPath2(), "utf8"));
|
|
25956
26803
|
} catch {
|
|
25957
26804
|
return [];
|
|
25958
26805
|
}
|
|
25959
26806
|
}
|
|
25960
|
-
function
|
|
26807
|
+
function save2(configs) {
|
|
25961
26808
|
const p = configPath2();
|
|
25962
|
-
|
|
26809
|
+
fs34.mkdirSync(path43.dirname(p), { recursive: true });
|
|
25963
26810
|
const tmp = `${p}.tmp`;
|
|
25964
|
-
|
|
25965
|
-
|
|
26811
|
+
fs34.writeFileSync(tmp, JSON.stringify(configs, null, 2) + "\n");
|
|
26812
|
+
fs34.renameSync(tmp, p);
|
|
25966
26813
|
}
|
|
25967
26814
|
function readProducerConfig(target) {
|
|
25968
|
-
return
|
|
26815
|
+
return load6().find((c) => c.target === target) ?? null;
|
|
25969
26816
|
}
|
|
25970
26817
|
function writeProducerConfig(config) {
|
|
25971
|
-
const configs =
|
|
26818
|
+
const configs = load6().filter((c) => c.target !== config.target);
|
|
25972
26819
|
configs.push(config);
|
|
25973
|
-
|
|
26820
|
+
save2(configs);
|
|
25974
26821
|
}
|
|
25975
26822
|
function clearProducerConfig(target) {
|
|
25976
|
-
|
|
26823
|
+
save2(load6().filter((c) => c.target !== target));
|
|
25977
26824
|
}
|
|
25978
26825
|
function listProducerConfigs() {
|
|
25979
|
-
return
|
|
26826
|
+
return load6();
|
|
25980
26827
|
}
|
|
25981
26828
|
|
|
25982
26829
|
// src/producers/core-adapter.ts
|
|
@@ -26315,8 +27162,19 @@ var hostCore = {
|
|
|
26315
27162
|
/** The ids of wairon's built-in architectural profiles, read from the core rules
|
|
26316
27163
|
* registry's built-in profile set (BUILTIN_PROFILES) — a pure, side-effect-free
|
|
26317
27164
|
* read of a bundled constant. */
|
|
26318
|
-
builtinProfileIds: () => [...BUILTIN_PROFILES]
|
|
27165
|
+
builtinProfileIds: () => [...BUILTIN_PROFILES],
|
|
27166
|
+
/** The ids of wairon's built-in COMPOSITE PROJECT KINDS, read from the core
|
|
27167
|
+
* rules registry's bundled constant (PROJECT_KINDS) — a pure, side-effect-free
|
|
27168
|
+
* read. The counterpart of builtinProfileIds: legal projectType values that
|
|
27169
|
+
* are not architectural profiles and carry no profile doctrine of their own,
|
|
27170
|
+
* so the hosted profile-application path recognizes a project kind as
|
|
27171
|
+
* resolvable-as-is (no contributing pack to adopt) instead of refusing it as
|
|
27172
|
+
* an unknown profile. */
|
|
27173
|
+
builtinProjectKinds: () => [...PROJECT_KINDS]
|
|
26319
27174
|
};
|
|
27175
|
+
function resolveContainedProjectPath(projectRoot2, projectPath) {
|
|
27176
|
+
return assertContainedProjectPath(projectRoot2, projectPath);
|
|
27177
|
+
}
|
|
26320
27178
|
function validateProjectAsComplete() {
|
|
26321
27179
|
const config = loadProjectConfig();
|
|
26322
27180
|
return validateAsComplete({ rules: config.rules, projectType: config.projectType });
|
|
@@ -26348,6 +27206,180 @@ var hostSdk = {
|
|
|
26348
27206
|
extractArchive: (archive, destDir, limits) => sdkPortal.extractPack(archive, destDir, limits)
|
|
26349
27207
|
};
|
|
26350
27208
|
|
|
27209
|
+
// src/server/projects.ts
|
|
27210
|
+
var ID_RE = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
27211
|
+
function isValidProjectId(id) {
|
|
27212
|
+
return typeof id === "string" && ID_RE.test(id);
|
|
27213
|
+
}
|
|
27214
|
+
function registryPath(dataDir) {
|
|
27215
|
+
return path44.join(dataDir, "projects.json");
|
|
27216
|
+
}
|
|
27217
|
+
function load7(dataDir) {
|
|
27218
|
+
try {
|
|
27219
|
+
return JSON.parse(fs35.readFileSync(registryPath(dataDir), "utf8"));
|
|
27220
|
+
} catch {
|
|
27221
|
+
return [];
|
|
27222
|
+
}
|
|
27223
|
+
}
|
|
27224
|
+
function save3(dataDir, records) {
|
|
27225
|
+
const p = registryPath(dataDir);
|
|
27226
|
+
fs35.mkdirSync(path44.dirname(p), { recursive: true });
|
|
27227
|
+
const tmp = `${p}.tmp`;
|
|
27228
|
+
fs35.writeFileSync(tmp, JSON.stringify(records, null, 2) + "\n");
|
|
27229
|
+
fs35.renameSync(tmp, p);
|
|
27230
|
+
}
|
|
27231
|
+
function projectRoot(dataDir, id) {
|
|
27232
|
+
return path44.join(dataDir, "projects", id);
|
|
27233
|
+
}
|
|
27234
|
+
function existingProjectRoot(dataDir, id) {
|
|
27235
|
+
if (!isValidProjectId(id)) return null;
|
|
27236
|
+
const rec = load7(dataDir).find((r) => r.id === id);
|
|
27237
|
+
return rec ? rec.rootPath : null;
|
|
27238
|
+
}
|
|
27239
|
+
function createProjectRecord(dataDir, id) {
|
|
27240
|
+
if (!isValidProjectId(id)) {
|
|
27241
|
+
throw new Error(`Invalid project id "${id}" (allowed: lowercase letters, digits, hyphen).`);
|
|
27242
|
+
}
|
|
27243
|
+
const records = load7(dataDir);
|
|
27244
|
+
if (records.some((r) => r.id === id)) {
|
|
27245
|
+
throw new Error(`Project "${id}" already exists.`);
|
|
27246
|
+
}
|
|
27247
|
+
const root = projectRoot(dataDir, id);
|
|
27248
|
+
fs35.mkdirSync(root, { recursive: true });
|
|
27249
|
+
const record2 = {
|
|
27250
|
+
id,
|
|
27251
|
+
rootPath: root,
|
|
27252
|
+
status: "active",
|
|
27253
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
27254
|
+
};
|
|
27255
|
+
records.push(record2);
|
|
27256
|
+
save3(dataDir, records);
|
|
27257
|
+
return record2;
|
|
27258
|
+
}
|
|
27259
|
+
function registerLocalDevProject(dataDir, id, rootPath) {
|
|
27260
|
+
if (!isValidProjectId(id)) {
|
|
27261
|
+
throw new Error(`Invalid project id "${id}" (allowed: lowercase letters, digits, hyphen).`);
|
|
27262
|
+
}
|
|
27263
|
+
const records = load7(dataDir);
|
|
27264
|
+
const existing = records.find((r) => r.id === id);
|
|
27265
|
+
const record2 = {
|
|
27266
|
+
id,
|
|
27267
|
+
rootPath,
|
|
27268
|
+
status: "active",
|
|
27269
|
+
createdAt: existing?.createdAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
27270
|
+
};
|
|
27271
|
+
const next = existing ? records.map((r) => r.id === id ? record2 : r) : [...records, record2];
|
|
27272
|
+
save3(dataDir, next);
|
|
27273
|
+
return record2;
|
|
27274
|
+
}
|
|
27275
|
+
function listProjectRecords(dataDir) {
|
|
27276
|
+
return load7(dataDir);
|
|
27277
|
+
}
|
|
27278
|
+
function removeProjectRecord(dataDir, id) {
|
|
27279
|
+
const records = load7(dataDir);
|
|
27280
|
+
const rec = records.find((r) => r.id === id);
|
|
27281
|
+
if (rec) {
|
|
27282
|
+
try {
|
|
27283
|
+
fs35.rmSync(rec.rootPath, { recursive: true, force: true });
|
|
27284
|
+
} catch {
|
|
27285
|
+
}
|
|
27286
|
+
}
|
|
27287
|
+
save3(dataDir, records.filter((r) => r.id !== id));
|
|
27288
|
+
}
|
|
27289
|
+
var SUBPROJECT_SEPARATOR = "::";
|
|
27290
|
+
function parseQualifiedSelector(value) {
|
|
27291
|
+
if (typeof value !== "string" || value.length === 0) return null;
|
|
27292
|
+
const [projectId, ...mounts] = value.split(SUBPROJECT_SEPARATOR);
|
|
27293
|
+
if (!isValidProjectId(projectId)) return null;
|
|
27294
|
+
if (mounts.some((m) => m.trim() === "")) return null;
|
|
27295
|
+
return { projectId, mounts };
|
|
27296
|
+
}
|
|
27297
|
+
function findSubsystemSpec(root, subsystemId) {
|
|
27298
|
+
const specsDir = aiPathsAt(root).specsDir();
|
|
27299
|
+
if (!fs35.existsSync(specsDir)) return null;
|
|
27300
|
+
for (const file of listFilesRecursive(specsDir, ".yaml")) {
|
|
27301
|
+
let raw;
|
|
27302
|
+
try {
|
|
27303
|
+
raw = readYamlFile(file);
|
|
27304
|
+
} catch {
|
|
27305
|
+
continue;
|
|
27306
|
+
}
|
|
27307
|
+
if (!raw || typeof raw !== "object" || !("parentSystem" in raw)) continue;
|
|
27308
|
+
if (raw.id !== subsystemId) continue;
|
|
27309
|
+
const pp = raw.projectPath;
|
|
27310
|
+
return typeof pp === "string" && pp.trim() !== "" ? { projectPath: pp } : {};
|
|
27311
|
+
}
|
|
27312
|
+
return null;
|
|
27313
|
+
}
|
|
27314
|
+
function resolveSubprojectMounts(projectId, projectRoot2, mounts) {
|
|
27315
|
+
let root = projectRoot2;
|
|
27316
|
+
let at = projectId;
|
|
27317
|
+
for (const mount of mounts) {
|
|
27318
|
+
const sub = findSubsystemSpec(root, mount);
|
|
27319
|
+
if (!sub) {
|
|
27320
|
+
throw new Error(
|
|
27321
|
+
`unknown subproject mount "${mount}" on "${at}" \u2014 no subsystem with that id exists in its spec tree`
|
|
27322
|
+
);
|
|
27323
|
+
}
|
|
27324
|
+
if (!sub.projectPath) {
|
|
27325
|
+
throw new Error(
|
|
27326
|
+
`subsystem "${mount}" on "${at}" is not a chained subproject (it carries no projectPath) \u2014 only a subsystem mounted via projectPath can be bound as a subproject`
|
|
27327
|
+
);
|
|
27328
|
+
}
|
|
27329
|
+
root = resolveContainedProjectPath(root, sub.projectPath);
|
|
27330
|
+
at = `${at}${SUBPROJECT_SEPARATOR}${mount}`;
|
|
27331
|
+
}
|
|
27332
|
+
return root;
|
|
27333
|
+
}
|
|
27334
|
+
function assertMintableNarrowingEntry(dataDir, entry) {
|
|
27335
|
+
if (entry === "*") return;
|
|
27336
|
+
const parsed = parseQualifiedSelector(entry);
|
|
27337
|
+
if (!parsed) {
|
|
27338
|
+
throw new Error(
|
|
27339
|
+
`invalid project narrowing entry "${entry}" (expected a project id, optionally subproject-qualified as projectId::subsystemId)`
|
|
27340
|
+
);
|
|
27341
|
+
}
|
|
27342
|
+
const rec = load7(dataDir).find((r) => r.id === parsed.projectId);
|
|
27343
|
+
if (!rec) throw new Error(`unknown project "${parsed.projectId}"`);
|
|
27344
|
+
if (parsed.mounts.length > 0) {
|
|
27345
|
+
resolveSubprojectMounts(parsed.projectId, rec.rootPath, parsed.mounts);
|
|
27346
|
+
}
|
|
27347
|
+
}
|
|
27348
|
+
function narrowingCovers(entry, target) {
|
|
27349
|
+
return target === entry || target.startsWith(entry + SUBPROJECT_SEPARATOR);
|
|
27350
|
+
}
|
|
27351
|
+
function resolveProjectBinding(dataDir, principal, selector) {
|
|
27352
|
+
const authorized = principal.projects;
|
|
27353
|
+
const wildcard = authorized.includes("*");
|
|
27354
|
+
let target;
|
|
27355
|
+
if (selector) {
|
|
27356
|
+
if (!wildcard && !authorized.some((e) => e !== "*" && narrowingCovers(e, selector))) return null;
|
|
27357
|
+
target = selector;
|
|
27358
|
+
} else if (!wildcard && authorized.length === 1) {
|
|
27359
|
+
target = authorized[0];
|
|
27360
|
+
} else {
|
|
27361
|
+
return null;
|
|
27362
|
+
}
|
|
27363
|
+
const parsed = parseQualifiedSelector(target);
|
|
27364
|
+
if (!parsed) return null;
|
|
27365
|
+
const rec = load7(dataDir).find((r) => r.id === parsed.projectId);
|
|
27366
|
+
if (!rec || rec.status !== "active") return null;
|
|
27367
|
+
let rootPath = rec.rootPath;
|
|
27368
|
+
if (parsed.mounts.length > 0) {
|
|
27369
|
+
try {
|
|
27370
|
+
rootPath = resolveSubprojectMounts(parsed.projectId, rec.rootPath, parsed.mounts);
|
|
27371
|
+
} catch {
|
|
27372
|
+
return null;
|
|
27373
|
+
}
|
|
27374
|
+
}
|
|
27375
|
+
const binding = { rootPath, projectId: parsed.projectId };
|
|
27376
|
+
if (parsed.mounts.length > 0) binding.subproject = parsed.mounts.join(SUBPROJECT_SEPARATOR);
|
|
27377
|
+
return binding;
|
|
27378
|
+
}
|
|
27379
|
+
function resolveProjectRoot(dataDir, principal, selector) {
|
|
27380
|
+
return resolveProjectBinding(dataDir, principal, selector)?.rootPath ?? null;
|
|
27381
|
+
}
|
|
27382
|
+
|
|
26351
27383
|
// src/server/errors.ts
|
|
26352
27384
|
var UnauthenticatedError = class extends Error {
|
|
26353
27385
|
constructor() {
|
|
@@ -26468,9 +27500,14 @@ function lockProject(cfg, credential, project2) {
|
|
|
26468
27500
|
}
|
|
26469
27501
|
return executeApprovedLock(cfg, project2);
|
|
26470
27502
|
}
|
|
26471
|
-
function
|
|
27503
|
+
function boundLifecycleRoot(cfg, projectId, subproject) {
|
|
26472
27504
|
const root = existingProjectRoot(cfg.dataDir, projectId);
|
|
26473
27505
|
if (!root) throw new Error(`Unknown project "${projectId}".`);
|
|
27506
|
+
if (!subproject) return root;
|
|
27507
|
+
return resolveSubprojectMounts(projectId, root, subproject.split(SUBPROJECT_SEPARATOR));
|
|
27508
|
+
}
|
|
27509
|
+
function executeApprovedLock(cfg, projectId, subproject) {
|
|
27510
|
+
const root = boundLifecycleRoot(cfg, projectId, subproject);
|
|
26474
27511
|
return runWithProjectRoot(root, () => {
|
|
26475
27512
|
hostGit.sync();
|
|
26476
27513
|
const result = validateProjectAsComplete();
|
|
@@ -26642,9 +27679,8 @@ function promoteProject(cfg, credential, project2) {
|
|
|
26642
27679
|
}
|
|
26643
27680
|
return executeApprovedPromote(cfg, project2);
|
|
26644
27681
|
}
|
|
26645
|
-
function executeApprovedPromote(cfg, projectId) {
|
|
26646
|
-
const root =
|
|
26647
|
-
if (!root) throw new Error(`Unknown project "${projectId}".`);
|
|
27682
|
+
function executeApprovedPromote(cfg, projectId, subproject) {
|
|
27683
|
+
const root = boundLifecycleRoot(cfg, projectId, subproject);
|
|
26648
27684
|
return runWithProjectRoot(root, () => {
|
|
26649
27685
|
const lock = hostCore.readLockRecord();
|
|
26650
27686
|
if (!lock) {
|
|
@@ -26759,6 +27795,35 @@ function storeListGlobalPacks() {
|
|
|
26759
27795
|
);
|
|
26760
27796
|
return [...instance, ...image];
|
|
26761
27797
|
}
|
|
27798
|
+
function scanGlobalPackProfiles() {
|
|
27799
|
+
const out = [];
|
|
27800
|
+
for (const dir of [hostCore.globalPacksDir(), imagePacksDir()]) {
|
|
27801
|
+
for (const full of hostCore.discoverPacks(dir)) {
|
|
27802
|
+
try {
|
|
27803
|
+
const loaded = hostCore.loadExtensionPacks([{ ref: full, scope: "global" }], path45.dirname(full));
|
|
27804
|
+
if (loaded.errors.length) continue;
|
|
27805
|
+
const source = loaded.packNames[0] ?? path45.basename(full);
|
|
27806
|
+
for (const [id, def] of Object.entries(loaded.profiles)) out.push({ id, source, family: def.family });
|
|
27807
|
+
} catch {
|
|
27808
|
+
}
|
|
27809
|
+
}
|
|
27810
|
+
}
|
|
27811
|
+
return out;
|
|
27812
|
+
}
|
|
27813
|
+
function scanProjectPackProfiles() {
|
|
27814
|
+
const root = getProjectRoot();
|
|
27815
|
+
const out = [];
|
|
27816
|
+
for (const ref of loadProjectConfig().extensions?.packs ?? []) {
|
|
27817
|
+
try {
|
|
27818
|
+
const loaded = hostCore.loadExtensionPacks([{ ref, scope: "project" }], root);
|
|
27819
|
+
if (loaded.errors.length) continue;
|
|
27820
|
+
const source = loaded.packNames[0] ?? stem(ref);
|
|
27821
|
+
for (const [id, def] of Object.entries(loaded.profiles)) out.push({ id, source, family: def.family });
|
|
27822
|
+
} catch {
|
|
27823
|
+
}
|
|
27824
|
+
}
|
|
27825
|
+
return out;
|
|
27826
|
+
}
|
|
26762
27827
|
function storeListAvailableProfiles() {
|
|
26763
27828
|
const out = [];
|
|
26764
27829
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -26769,19 +27834,21 @@ function storeListAvailableProfiles() {
|
|
|
26769
27834
|
out.push(family ? { id, source, family } : { id, source });
|
|
26770
27835
|
};
|
|
26771
27836
|
for (const id of hostCore.builtinProfileIds()) emit(id, "builtin");
|
|
26772
|
-
const
|
|
26773
|
-
|
|
26774
|
-
|
|
26775
|
-
|
|
26776
|
-
|
|
26777
|
-
|
|
26778
|
-
|
|
26779
|
-
|
|
26780
|
-
|
|
26781
|
-
|
|
27837
|
+
for (const c of scanGlobalPackProfiles()) emit(c.id, c.source, c.family);
|
|
27838
|
+
return out;
|
|
27839
|
+
}
|
|
27840
|
+
function storeListProjectProfiles() {
|
|
27841
|
+
const out = [];
|
|
27842
|
+
const seen = /* @__PURE__ */ new Set();
|
|
27843
|
+
const emit = (id, source, installed, family) => {
|
|
27844
|
+
const key = JSON.stringify([id, source]);
|
|
27845
|
+
if (seen.has(key)) return;
|
|
27846
|
+
seen.add(key);
|
|
27847
|
+
out.push({ id, source, ...family ? { family } : {}, installed });
|
|
26782
27848
|
};
|
|
26783
|
-
|
|
26784
|
-
|
|
27849
|
+
for (const id of hostCore.builtinProfileIds()) emit(id, "builtin", true);
|
|
27850
|
+
for (const c of scanProjectPackProfiles()) emit(c.id, c.source, true, c.family);
|
|
27851
|
+
for (const c of scanGlobalPackProfiles()) emit(c.id, c.source, false, c.family);
|
|
26785
27852
|
return out;
|
|
26786
27853
|
}
|
|
26787
27854
|
function readPackContent(full) {
|
|
@@ -26974,6 +28041,10 @@ function listAvailableProfiles(cfg, credential) {
|
|
|
26974
28041
|
requirePrincipal2(cfg, credential);
|
|
26975
28042
|
return storeListAvailableProfiles();
|
|
26976
28043
|
}
|
|
28044
|
+
function listProjectProfiles(cfg, credential, project2) {
|
|
28045
|
+
requireCap(cfg, credential, "project:read", "project", project2, "Forbidden \u2014 listing a project's selectable profiles requires project:read over the project");
|
|
28046
|
+
return executeApprovedListProjectProfiles(cfg, project2);
|
|
28047
|
+
}
|
|
26977
28048
|
function listAdoptableProjectPacks(cfg, credential, project2) {
|
|
26978
28049
|
requireCap(cfg, credential, "project:read", "project", project2, "Forbidden \u2014 listing adoptable packs requires project:read over the project");
|
|
26979
28050
|
return storeListGlobalPacks();
|
|
@@ -26996,6 +28067,26 @@ function executeApprovedInstallProjectPack(cfg, project2, name, content) {
|
|
|
26996
28067
|
function executeApprovedResolveGlobalPacks(names) {
|
|
26997
28068
|
return storeResolveGlobalPacks(names);
|
|
26998
28069
|
}
|
|
28070
|
+
function executeApprovedListProjectProfiles(cfg, project2) {
|
|
28071
|
+
return runWithProjectRoot(boundProject2(cfg, project2), () => storeListProjectProfiles());
|
|
28072
|
+
}
|
|
28073
|
+
function executeApprovedEnsureProfileInstalled(cfg, project2, profileId) {
|
|
28074
|
+
if (hostCore.builtinProjectKinds().includes(profileId) || hostCore.builtinProfileIds().includes(profileId)) {
|
|
28075
|
+
return { profileId, source: "builtin" };
|
|
28076
|
+
}
|
|
28077
|
+
const contributors = executeApprovedListProjectProfiles(cfg, project2).filter((p) => p.id === profileId);
|
|
28078
|
+
const installed = contributors.find((p) => p.installed);
|
|
28079
|
+
if (installed) return { profileId, source: installed.source };
|
|
28080
|
+
const adoptable = contributors[0];
|
|
28081
|
+
const resolved = adoptable ? executeApprovedResolveGlobalPacks([adoptable.source]).resolved[0] : void 0;
|
|
28082
|
+
if (!resolved) {
|
|
28083
|
+
throw new Error(
|
|
28084
|
+
`Unknown profile "${profileId}" \u2014 no built-in profile or project kind carries it, no pack registered in project "${project2}" contributes it, and no server-global pack (mutable instance tier or immutable image tier) contributes it. Writing an unresolvable id as the projectType would silently disable the whole profile doctrine (UNKNOWN_PROFILE), so it is refused instead of applied.`
|
|
28085
|
+
);
|
|
28086
|
+
}
|
|
28087
|
+
executeApprovedInstallProjectPack(cfg, project2, resolved.name, resolved.content);
|
|
28088
|
+
return { profileId, source: resolved.name, adoptedPackName: resolved.name };
|
|
28089
|
+
}
|
|
26999
28090
|
function removeProjectPack(cfg, credential, project2, name) {
|
|
27000
28091
|
requireCap(cfg, credential, "project:admin", "project", project2, "Forbidden \u2014 removing a project pack requires project:admin over the project");
|
|
27001
28092
|
runWithProjectRoot(boundProject2(cfg, project2), () => storeRemoveProjectPack(name));
|
|
@@ -27990,6 +29081,40 @@ function requestPackNames(request) {
|
|
|
27990
29081
|
if (!sel) return [];
|
|
27991
29082
|
return [.../* @__PURE__ */ new Set([...sel.requiredPackNames ?? [], ...sel.defaultPackNames ?? []])];
|
|
27992
29083
|
}
|
|
29084
|
+
function classifyProfile(projectType, catalog) {
|
|
29085
|
+
if (hostCore.builtinProfileIds().includes(projectType) || hostCore.builtinProjectKinds().includes(projectType)) {
|
|
29086
|
+
return { source: "builtin", resolvable: true };
|
|
29087
|
+
}
|
|
29088
|
+
const installed = catalog.find((p) => p.id === projectType && p.installed);
|
|
29089
|
+
if (installed) return { source: installed.source, resolvable: true };
|
|
29090
|
+
return { resolvable: false };
|
|
29091
|
+
}
|
|
29092
|
+
function firstApplicableProfileId(candidates, catalog) {
|
|
29093
|
+
const kinds = hostCore.builtinProjectKinds();
|
|
29094
|
+
const catalogIds = new Set(catalog.map((p) => p.id));
|
|
29095
|
+
return candidates.find((id) => kinds.includes(id) || catalogIds.has(id));
|
|
29096
|
+
}
|
|
29097
|
+
function unappliedIds(selectedProfileIds, governingProfileId) {
|
|
29098
|
+
return selectedProfileIds.filter((id) => id !== governingProfileId);
|
|
29099
|
+
}
|
|
29100
|
+
function foldAppliedProfile(root, profileId, actor) {
|
|
29101
|
+
const recorded = readProjectProfileSelection(root);
|
|
29102
|
+
const folded = {
|
|
29103
|
+
profileIds: [profileId, ...unappliedIds(recorded?.profileIds ?? [], profileId)],
|
|
29104
|
+
requiredPackNames: recorded?.requiredPackNames ?? [],
|
|
29105
|
+
selectedBy: actor,
|
|
29106
|
+
selectedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
29107
|
+
};
|
|
29108
|
+
if (recorded?.defaultPackNames) folded.defaultPackNames = recorded.defaultPackNames;
|
|
29109
|
+
recordProjectProfileSelection(root, folded);
|
|
29110
|
+
return folded;
|
|
29111
|
+
}
|
|
29112
|
+
function overridingSubsystemIds(root) {
|
|
29113
|
+
return runWithProjectRoot(
|
|
29114
|
+
root,
|
|
29115
|
+
() => hostCore.loadSubsystemSpecs().filter((s) => !!s.profile).map((s) => s.id)
|
|
29116
|
+
);
|
|
29117
|
+
}
|
|
27993
29118
|
function resolvedSelection(request, policy, selectedBy) {
|
|
27994
29119
|
const sel = request.profileSelection;
|
|
27995
29120
|
const selection = {
|
|
@@ -28008,7 +29133,8 @@ function buildEvaluation(input) {
|
|
|
28008
29133
|
selectedProfileIds,
|
|
28009
29134
|
hasSelection,
|
|
28010
29135
|
countMissingPacksAsViolation,
|
|
28011
|
-
requiredDefaultResolution
|
|
29136
|
+
requiredDefaultResolution,
|
|
29137
|
+
governingProfileId
|
|
28012
29138
|
} = input;
|
|
28013
29139
|
const present = new Set(presentPackNames);
|
|
28014
29140
|
let missingPackNames;
|
|
@@ -28029,6 +29155,7 @@ function buildEvaluation(input) {
|
|
|
28029
29155
|
const allowed = policy.allowedProfileIds;
|
|
28030
29156
|
const disallowedProfileIds = allowed && allowed.length > 0 ? selectedProfileIds.filter((id) => !allowed.includes(id)) : [];
|
|
28031
29157
|
const selectionRequiredUnmet = policy.requireProfileSelection && !hasSelection;
|
|
29158
|
+
const unappliedProfileIds = governingProfileId ? unappliedIds(selectedProfileIds, governingProfileId) : [];
|
|
28032
29159
|
const messages = [];
|
|
28033
29160
|
if (selectionRequiredUnmet) {
|
|
28034
29161
|
messages.push("Profile selection is required by policy but none was provided.");
|
|
@@ -28040,16 +29167,24 @@ function buildEvaluation(input) {
|
|
|
28040
29167
|
for (const n of blockedPackNames) messages.push(`Pack "${n}" is blocked by policy.`);
|
|
28041
29168
|
for (const id of missingProfileIds) messages.push(`Required profile "${id}" is not selected.`);
|
|
28042
29169
|
for (const id of disallowedProfileIds) messages.push(`Profile "${id}" is not permitted by policy.`);
|
|
29170
|
+
for (const id of unappliedProfileIds) {
|
|
29171
|
+
messages.push(
|
|
29172
|
+
`Selected profile "${id}" is recorded but does not govern the project (projectType is "${governingProfileId}") \u2014 a project has exactly one governing profile.`
|
|
29173
|
+
);
|
|
29174
|
+
}
|
|
28043
29175
|
const violation = selectionRequiredUnmet || blockedPackNames.length > 0 || missingProfileIds.length > 0 || disallowedProfileIds.length > 0 || countMissingPacksAsViolation && (missingPackNames.length > 0 || unresolvedPacks.length > 0);
|
|
28044
|
-
|
|
29176
|
+
const result = {
|
|
28045
29177
|
compliant: !violation,
|
|
28046
29178
|
mode: policy.enforcementMode,
|
|
28047
29179
|
missingPackNames,
|
|
28048
29180
|
blockedPackNames,
|
|
28049
29181
|
missingProfileIds,
|
|
28050
29182
|
unresolvedPacks,
|
|
29183
|
+
unappliedProfileIds,
|
|
28051
29184
|
messages
|
|
28052
29185
|
};
|
|
29186
|
+
if (governingProfileId) result.governingProfileId = governingProfileId;
|
|
29187
|
+
return result;
|
|
28053
29188
|
}
|
|
28054
29189
|
function performInit(cfg, request, principal) {
|
|
28055
29190
|
const policy = effectivePolicy(cfg.dataDir);
|
|
@@ -28069,17 +29204,24 @@ function performInit(cfg, request, principal) {
|
|
|
28069
29204
|
request.ownerUnitId,
|
|
28070
29205
|
principal ? principalSubject3(principal) : void 0
|
|
28071
29206
|
);
|
|
28072
|
-
|
|
28073
|
-
|
|
28074
|
-
|
|
28075
|
-
|
|
28076
|
-
|
|
28077
|
-
|
|
28078
|
-
...requestPackNames(request)
|
|
28079
|
-
])
|
|
28080
|
-
);
|
|
29207
|
+
const packResolution = executeApprovedResolveGlobalPacks([
|
|
29208
|
+
...policy.requiredGlobalPacks,
|
|
29209
|
+
...policy.defaultProjectPacks,
|
|
29210
|
+
...requestPackNames(request)
|
|
29211
|
+
]);
|
|
29212
|
+
installResolvedPacks(cfg, record2.id, packResolution);
|
|
28081
29213
|
const selectedBy = principal ? principalSubject3(principal) : void 0;
|
|
28082
|
-
|
|
29214
|
+
const selection = resolvedSelection(request, policy, selectedBy);
|
|
29215
|
+
recordProjectProfileSelection(record2.rootPath, selection);
|
|
29216
|
+
const catalog = executeApprovedListProjectProfiles(cfg, record2.id);
|
|
29217
|
+
const appliedProfileId = firstApplicableProfileId(selection.profileIds, catalog);
|
|
29218
|
+
let appliedSource;
|
|
29219
|
+
if (appliedProfileId) {
|
|
29220
|
+
const application = executeApprovedEnsureProfileInstalled(cfg, record2.id, appliedProfileId);
|
|
29221
|
+
writeProjectType(record2.rootPath, application.profileId);
|
|
29222
|
+
appliedSource = application.source;
|
|
29223
|
+
}
|
|
29224
|
+
const unapplied = appliedProfileId ? unappliedIds(selection.profileIds, appliedProfileId) : [...selection.profileIds];
|
|
28083
29225
|
const actor = principal ? principalSubject3(principal) : SYSTEM_SUBJECT;
|
|
28084
29226
|
tryAppendAudit2(
|
|
28085
29227
|
cfg,
|
|
@@ -28088,7 +29230,18 @@ function performInit(cfg, request, principal) {
|
|
|
28088
29230
|
"project.init.policy",
|
|
28089
29231
|
"info",
|
|
28090
29232
|
"project",
|
|
28091
|
-
{
|
|
29233
|
+
{
|
|
29234
|
+
target: record2.id,
|
|
29235
|
+
projectId: record2.id,
|
|
29236
|
+
metadata: JSON.stringify({
|
|
29237
|
+
...appliedProfileId ? { appliedProfileId, profileSource: appliedSource } : {
|
|
29238
|
+
appliedProfileId: null,
|
|
29239
|
+
profileNotApplied: selection.profileIds.length === 0 ? "no profile was selected \u2014 the default projectType stands" : "no selected profile is resolvable on this instance \u2014 the default projectType stands"
|
|
29240
|
+
},
|
|
29241
|
+
...unapplied.length > 0 ? { unappliedProfileIds: unapplied } : {},
|
|
29242
|
+
...packResolution.unresolved.length > 0 ? { unresolvedPacks: packResolution.unresolved } : {}
|
|
29243
|
+
})
|
|
29244
|
+
},
|
|
28092
29245
|
principal?.tokenId
|
|
28093
29246
|
)
|
|
28094
29247
|
);
|
|
@@ -28129,6 +29282,7 @@ function evaluateProjectPolicy(cfg, credential, projectId) {
|
|
|
28129
29282
|
const root = resolveProjectRoot(cfg.dataDir, principal, projectId);
|
|
28130
29283
|
if (!root) throw new Error(`Unknown project "${projectId}".`);
|
|
28131
29284
|
const policy = effectivePolicy(cfg.dataDir);
|
|
29285
|
+
const governingProfileId = readProjectType(root);
|
|
28132
29286
|
const selection = readProjectProfileSelection(root);
|
|
28133
29287
|
return buildEvaluation({
|
|
28134
29288
|
policy,
|
|
@@ -28136,7 +29290,8 @@ function evaluateProjectPolicy(cfg, credential, projectId) {
|
|
|
28136
29290
|
selectedProfileIds: selection?.profileIds ?? [],
|
|
28137
29291
|
hasSelection: !!selection,
|
|
28138
29292
|
countMissingPacksAsViolation: true,
|
|
28139
|
-
requiredDefaultResolution: executeApprovedResolveGlobalPacks(requiredDefaultNames(policy))
|
|
29293
|
+
requiredDefaultResolution: executeApprovedResolveGlobalPacks(requiredDefaultNames(policy)),
|
|
29294
|
+
governingProfileId
|
|
28140
29295
|
});
|
|
28141
29296
|
}
|
|
28142
29297
|
function reconcileProjectPolicy(cfg, credential, projectId) {
|
|
@@ -28151,20 +29306,41 @@ function reconcileProjectPolicy(cfg, credential, projectId) {
|
|
|
28151
29306
|
const policy = effectivePolicy(cfg.dataDir);
|
|
28152
29307
|
const selection = readProjectProfileSelection(root);
|
|
28153
29308
|
const resolution = executeApprovedResolveGlobalPacks(requiredDefaultNames(policy));
|
|
28154
|
-
|
|
28155
|
-
const installedSet = new Set(installed);
|
|
29309
|
+
const installedSet = new Set(installedPackNames(cfg, projectId));
|
|
28156
29310
|
const toApply = resolution.resolved.filter((p) => !installedSet.has(p.name));
|
|
29311
|
+
const appliedPackNames = toApply.map((p) => p.name);
|
|
28157
29312
|
if (toApply.length > 0) {
|
|
28158
29313
|
installResolvedPacks(cfg, projectId, { resolved: toApply, unresolved: [] });
|
|
28159
|
-
|
|
29314
|
+
}
|
|
29315
|
+
const catalog = executeApprovedListProjectProfiles(cfg, projectId);
|
|
29316
|
+
const previousProfileId = readProjectType(root);
|
|
29317
|
+
let governingProfileId = previousProfileId;
|
|
29318
|
+
const requiredProfileIds = policy.requiredProfileIds ?? [];
|
|
29319
|
+
const policyUnsatisfied = requiredProfileIds.length > 0 && !requiredProfileIds.includes(governingProfileId);
|
|
29320
|
+
const governingUnresolvable = !classifyProfile(governingProfileId, catalog).resolvable;
|
|
29321
|
+
let repairedProfileId;
|
|
29322
|
+
let selectedProfileIds = selection?.profileIds ?? [];
|
|
29323
|
+
if (policyUnsatisfied || governingUnresolvable) {
|
|
29324
|
+
const target = firstApplicableProfileId(
|
|
29325
|
+
[...requiredProfileIds, ...selection?.profileIds ?? []],
|
|
29326
|
+
catalog
|
|
29327
|
+
);
|
|
29328
|
+
if (target) {
|
|
29329
|
+
const application = executeApprovedEnsureProfileInstalled(cfg, projectId, target);
|
|
29330
|
+
writeProjectType(root, application.profileId);
|
|
29331
|
+
governingProfileId = application.profileId;
|
|
29332
|
+
repairedProfileId = application.profileId;
|
|
29333
|
+
selectedProfileIds = foldAppliedProfile(root, application.profileId, principalSubject3(principal)).profileIds;
|
|
29334
|
+
}
|
|
28160
29335
|
}
|
|
28161
29336
|
const result = buildEvaluation({
|
|
28162
29337
|
policy,
|
|
28163
|
-
presentPackNames:
|
|
28164
|
-
selectedProfileIds
|
|
29338
|
+
presentPackNames: installedPackNames(cfg, projectId),
|
|
29339
|
+
selectedProfileIds,
|
|
28165
29340
|
hasSelection: !!selection,
|
|
28166
29341
|
countMissingPacksAsViolation: true,
|
|
28167
|
-
requiredDefaultResolution: resolution
|
|
29342
|
+
requiredDefaultResolution: resolution,
|
|
29343
|
+
governingProfileId
|
|
28168
29344
|
});
|
|
28169
29345
|
tryAppendAudit2(
|
|
28170
29346
|
cfg,
|
|
@@ -28173,7 +29349,15 @@ function reconcileProjectPolicy(cfg, credential, projectId) {
|
|
|
28173
29349
|
"policy.reconcile",
|
|
28174
29350
|
"info",
|
|
28175
29351
|
"policy",
|
|
28176
|
-
{
|
|
29352
|
+
{
|
|
29353
|
+
target: projectId,
|
|
29354
|
+
projectId,
|
|
29355
|
+
metadata: JSON.stringify({
|
|
29356
|
+
...appliedPackNames.length > 0 ? { appliedPackNames } : {},
|
|
29357
|
+
...repairedProfileId ? { repairedProfileId, previousProfileId } : {},
|
|
29358
|
+
...resolution.unresolved.length > 0 ? { unresolvedPacks: resolution.unresolved } : {}
|
|
29359
|
+
})
|
|
29360
|
+
},
|
|
28177
29361
|
principal.tokenId
|
|
28178
29362
|
)
|
|
28179
29363
|
);
|
|
@@ -28189,8 +29373,19 @@ function getProjectConfig(cfg, credential, projectId) {
|
|
|
28189
29373
|
const root = resolveProjectRoot(cfg.dataDir, principal, projectId);
|
|
28190
29374
|
if (!root) throw new Error(`Unknown project "${projectId}".`);
|
|
28191
29375
|
const projectType = readProjectType(root);
|
|
29376
|
+
const selection = readProjectProfileSelection(root);
|
|
29377
|
+
const classified = classifyProfile(projectType, executeApprovedListProjectProfiles(cfg, projectId));
|
|
28192
29378
|
const locked = runWithProjectRoot(root, () => hostCore.readLockRecord() !== null);
|
|
28193
|
-
|
|
29379
|
+
const overriding = overridingSubsystemIds(root);
|
|
29380
|
+
const view = {
|
|
29381
|
+
projectType,
|
|
29382
|
+
locked,
|
|
29383
|
+
profileResolvable: classified.resolvable,
|
|
29384
|
+
unappliedProfileIds: unappliedIds(selection?.profileIds ?? [], projectType),
|
|
29385
|
+
overridingSubsystemIds: overriding
|
|
29386
|
+
};
|
|
29387
|
+
if (classified.source) view.profileSource = classified.source;
|
|
29388
|
+
return view;
|
|
28194
29389
|
}
|
|
28195
29390
|
function setProjectType(cfg, credential, projectId, projectType) {
|
|
28196
29391
|
const principal = requirePrincipal4(cfg, credential);
|
|
@@ -28201,9 +29396,23 @@ function setProjectType(cfg, credential, projectId, projectType) {
|
|
|
28201
29396
|
}
|
|
28202
29397
|
const root = resolveProjectRoot(cfg.dataDir, principal, projectId);
|
|
28203
29398
|
if (!root) throw new Error(`Unknown project "${projectId}".`);
|
|
28204
|
-
|
|
29399
|
+
const application = executeApprovedEnsureProfileInstalled(cfg, projectId, projectType);
|
|
29400
|
+
writeProjectType(root, application.profileId);
|
|
29401
|
+
const folded = foldAppliedProfile(root, application.profileId, principalSubject3(principal));
|
|
29402
|
+
const remainder = folded.profileIds.slice(1);
|
|
28205
29403
|
const locked = runWithProjectRoot(root, () => hostCore.readLockRecord() !== null);
|
|
28206
|
-
|
|
29404
|
+
const overriding = overridingSubsystemIds(root);
|
|
29405
|
+
const view = {
|
|
29406
|
+
projectType: application.profileId,
|
|
29407
|
+
locked,
|
|
29408
|
+
profileSource: application.source,
|
|
29409
|
+
// The write path guarantees resolvability — the ensure seam refused anything else.
|
|
29410
|
+
profileResolvable: true,
|
|
29411
|
+
unappliedProfileIds: remainder,
|
|
29412
|
+
overridingSubsystemIds: overriding
|
|
29413
|
+
};
|
|
29414
|
+
if (application.adoptedPackName) view.adoptedPackName = application.adoptedPackName;
|
|
29415
|
+
return view;
|
|
28207
29416
|
}
|
|
28208
29417
|
function getPackPolicy(cfg, credential) {
|
|
28209
29418
|
requirePrincipal4(cfg, credential);
|
|
@@ -28373,11 +29582,8 @@ function mintToken(cfg, credential, request) {
|
|
|
28373
29582
|
}
|
|
28374
29583
|
assertNotReservedSubjectId(cfg, [request.ownerUserId]);
|
|
28375
29584
|
const projects = request.projects?.length ? request.projects : ["*"];
|
|
28376
|
-
const knownProjects = new Set(listProjectRecords(cfg.dataDir).map((p) => p.id));
|
|
28377
29585
|
for (const p of projects) {
|
|
28378
|
-
|
|
28379
|
-
throw new Error(`unknown project "${p}"`);
|
|
28380
|
-
}
|
|
29586
|
+
assertMintableNarrowingEntry(cfg.dataDir, p);
|
|
28381
29587
|
}
|
|
28382
29588
|
const owner = findUserByRecordOrSubjectId(cfg.dataDir, request.ownerUserId);
|
|
28383
29589
|
if (owner && owner.status !== "active") {
|
|
@@ -28414,12 +29620,21 @@ function revokeToken(cfg, credential, tokenId) {
|
|
|
28414
29620
|
}
|
|
28415
29621
|
function mintSelfToken(cfg, credential, projectId, write) {
|
|
28416
29622
|
const principal = requirePrincipal5(cfg, credential);
|
|
28417
|
-
|
|
29623
|
+
const parsed = parseQualifiedSelector(projectId);
|
|
29624
|
+
if (!parsed) {
|
|
29625
|
+
throw new Error(
|
|
29626
|
+
`invalid project id "${projectId}" (expected a project id, optionally subproject-qualified as projectId::subsystemId)`
|
|
29627
|
+
);
|
|
29628
|
+
}
|
|
29629
|
+
if (authorize(cfg.dataDir, principal, PROJECT_READ_CAPABILITY2, "project", parsed.projectId).value !== "yes") {
|
|
28418
29630
|
throw new ForbiddenError("caller lacks project:read on the requested project");
|
|
28419
29631
|
}
|
|
28420
|
-
if (write && authorize(cfg.dataDir, principal, PROJECT_WRITE_CAPABILITY2, "project", projectId).value !== "yes") {
|
|
29632
|
+
if (write && authorize(cfg.dataDir, principal, PROJECT_WRITE_CAPABILITY2, "project", parsed.projectId).value !== "yes") {
|
|
28421
29633
|
throw new ForbiddenError("caller lacks project:write on the requested project");
|
|
28422
29634
|
}
|
|
29635
|
+
if (parsed.mounts.length > 0) {
|
|
29636
|
+
assertMintableNarrowingEntry(cfg.dataDir, projectId);
|
|
29637
|
+
}
|
|
28423
29638
|
const token = "wk_" + crypto10.randomBytes(24).toString("hex");
|
|
28424
29639
|
const owner = auditActor(principal);
|
|
28425
29640
|
const record2 = {
|
|
@@ -29031,12 +30246,12 @@ function resolveVisibility(observerProjectId, units, placements) {
|
|
|
29031
30246
|
const best = /* @__PURE__ */ new Map();
|
|
29032
30247
|
for (const placement of placements) {
|
|
29033
30248
|
if (placement.projectId === observerProjectId) continue;
|
|
29034
|
-
const
|
|
29035
|
-
if (!
|
|
29036
|
-
const closedOk =
|
|
30249
|
+
const path61 = chainOf(placement.unitId, unitById);
|
|
30250
|
+
if (!path61.length) continue;
|
|
30251
|
+
const closedOk = path61.every((u) => effectivePosture(u, unitById) !== "closed" || observerInside(u.id) || grantedTo(u));
|
|
29037
30252
|
if (!closedOk) continue;
|
|
29038
|
-
const crossTenant = !tenantRoots.has(
|
|
29039
|
-
if (crossTenant && !
|
|
30253
|
+
const crossTenant = !tenantRoots.has(path61[path61.length - 1].id);
|
|
30254
|
+
if (crossTenant && !path61.some(grantedTo)) continue;
|
|
29040
30255
|
if (crossTenant && directUnits.length === 0) continue;
|
|
29041
30256
|
const distance = crossTenant ? "partner" : sameBranch(placement.unitId) ? "department" : "instance";
|
|
29042
30257
|
const existing = best.get(placement.projectId);
|
|
@@ -29059,6 +30274,17 @@ function audienceDistance(resolution, targetProjectId) {
|
|
|
29059
30274
|
|
|
29060
30275
|
// src/server/landscape.ts
|
|
29061
30276
|
var yamlLib = __toESM(require("js-yaml"));
|
|
30277
|
+
init_filenames();
|
|
30278
|
+
|
|
30279
|
+
// src/server/openapiindex.ts
|
|
30280
|
+
function buildOpenApiSpecIndex(specs) {
|
|
30281
|
+
return { openapiIndex: true, specs: specs.map((s) => ({ portalId: s.portalId, name: s.name })) };
|
|
30282
|
+
}
|
|
30283
|
+
function openApiIndexDocument(specs) {
|
|
30284
|
+
return JSON.stringify(buildOpenApiSpecIndex(specs), null, 2);
|
|
30285
|
+
}
|
|
30286
|
+
|
|
30287
|
+
// src/server/landscape.ts
|
|
29062
30288
|
var PROJECT_ADMIN_CAPABILITY3 = "project:admin";
|
|
29063
30289
|
var PROJECT_READ_CAPABILITY3 = "project:read";
|
|
29064
30290
|
var PROJECT_WRITE_CAPABILITY3 = "project:write";
|
|
@@ -29545,7 +30771,7 @@ function getProjectSurfaceForMcp(cfg, credential, currentProjectId, targetProjec
|
|
|
29545
30771
|
const result = runWithProjectRoot(record2.rootPath, () => hostSurfaces.exportBoundSurface(maxAudience, "native"));
|
|
29546
30772
|
return { ...result.snapshot, origin: "exchanged" };
|
|
29547
30773
|
}
|
|
29548
|
-
function exportProjectSurface(cfg, credential, projectId, format, maxAudience) {
|
|
30774
|
+
function exportProjectSurface(cfg, credential, projectId, format, maxAudience, portalId) {
|
|
29549
30775
|
const principal = requirePrincipal6(cfg, credential);
|
|
29550
30776
|
if (!permitsCap(cfg, principal, PROJECT_ADMIN_CAPABILITY3, "project", projectId)) {
|
|
29551
30777
|
throw new ForbiddenError(
|
|
@@ -29559,16 +30785,37 @@ function exportProjectSurface(cfg, credential, projectId, format, maxAudience) {
|
|
|
29559
30785
|
if (!root) throw new Error(`Unknown project "${projectId}".`);
|
|
29560
30786
|
const result = runWithProjectRoot(root, () => hostSurfaces.exportBoundSurface(maxAudience, format));
|
|
29561
30787
|
if (format === "openapi") {
|
|
30788
|
+
const specs = result.renderedSet ?? [];
|
|
30789
|
+
if (portalId) {
|
|
30790
|
+
const hit = specs.find((s) => s.portalId === portalId);
|
|
30791
|
+
if (!hit) {
|
|
30792
|
+
throw new Error(
|
|
30793
|
+
`Unknown portal "${portalId}" in project "${projectId}" (published: ${specs.map((s) => s.portalId).join(", ") || "none"}).`
|
|
30794
|
+
);
|
|
30795
|
+
}
|
|
30796
|
+
return {
|
|
30797
|
+
body: hit.document,
|
|
30798
|
+
contentType: "application/json",
|
|
30799
|
+
filename: `${safeFilenamePart(projectId)}-${safeFilenamePart(portalId)}-surface.openapi.json`
|
|
30800
|
+
};
|
|
30801
|
+
}
|
|
30802
|
+
if (specs.length === 1) {
|
|
30803
|
+
return {
|
|
30804
|
+
body: specs[0].document,
|
|
30805
|
+
contentType: "application/json",
|
|
30806
|
+
filename: `${safeFilenamePart(projectId)}-surface.openapi.json`
|
|
30807
|
+
};
|
|
30808
|
+
}
|
|
29562
30809
|
return {
|
|
29563
|
-
body:
|
|
30810
|
+
body: openApiIndexDocument(specs),
|
|
29564
30811
|
contentType: "application/json",
|
|
29565
|
-
filename: `${projectId}-surface.openapi.json`
|
|
30812
|
+
filename: `${safeFilenamePart(projectId)}-surface.openapi.index.json`
|
|
29566
30813
|
};
|
|
29567
30814
|
}
|
|
29568
30815
|
return {
|
|
29569
30816
|
body: yamlLib.dump(result.snapshot),
|
|
29570
30817
|
contentType: "application/yaml",
|
|
29571
|
-
filename: `${projectId}-surface.yaml`
|
|
30818
|
+
filename: `${safeFilenamePart(projectId)}-surface.yaml`
|
|
29572
30819
|
};
|
|
29573
30820
|
}
|
|
29574
30821
|
function removeRelation(cfg, credential, id) {
|
|
@@ -29612,7 +30859,8 @@ function handleLandscapeRequest(cfg, credential, req, res, body, url) {
|
|
|
29612
30859
|
credential,
|
|
29613
30860
|
parts[2],
|
|
29614
30861
|
url.searchParams.get("format") ?? "native",
|
|
29615
|
-
url.searchParams.get("audience") ?? "instance"
|
|
30862
|
+
url.searchParams.get("audience") ?? "instance",
|
|
30863
|
+
url.searchParams.get("spec") ?? void 0
|
|
29616
30864
|
);
|
|
29617
30865
|
res.writeHead(200, {
|
|
29618
30866
|
"content-type": artifact.contentType,
|
|
@@ -29942,10 +31190,9 @@ function migratePermissionModel(dataDir, apply) {
|
|
|
29942
31190
|
// src/server/http.ts
|
|
29943
31191
|
var http2 = __toESM(require("http"));
|
|
29944
31192
|
var fs49 = __toESM(require("fs"));
|
|
29945
|
-
var
|
|
31193
|
+
var path58 = __toESM(require("path"));
|
|
29946
31194
|
|
|
29947
31195
|
// src/server/request.ts
|
|
29948
|
-
var path58 = __toESM(require("path"));
|
|
29949
31196
|
var import_streamableHttp = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
|
|
29950
31197
|
init_fs();
|
|
29951
31198
|
|
|
@@ -30318,38 +31565,58 @@ function initializeProject(cfg, credential, request) {
|
|
|
30318
31565
|
}
|
|
30319
31566
|
}
|
|
30320
31567
|
}
|
|
30321
|
-
function lockProject2(cfg, credential, projectId) {
|
|
31568
|
+
function lockProject2(cfg, credential, projectId, subproject) {
|
|
30322
31569
|
return lifecycleAction(cfg, credential, projectId, {
|
|
30323
31570
|
action: "project:lock",
|
|
30324
31571
|
verb: "Lock",
|
|
30325
31572
|
noun: "lock",
|
|
31573
|
+
subproject,
|
|
30326
31574
|
execute: () => {
|
|
30327
|
-
const lock = executeApprovedLock(cfg, projectId);
|
|
31575
|
+
const lock = executeApprovedLock(cfg, projectId, subproject);
|
|
30328
31576
|
return {
|
|
30329
31577
|
status: "completed",
|
|
30330
31578
|
action: "project:lock",
|
|
30331
|
-
summary: `Locked project "${projectId}" (status: ${lock.status}).`,
|
|
31579
|
+
summary: `Locked project "${projectId}"${subprojectSuffix(subproject)} (status: ${lock.status}).`,
|
|
30332
31580
|
lock
|
|
30333
31581
|
};
|
|
30334
31582
|
}
|
|
30335
31583
|
});
|
|
30336
31584
|
}
|
|
30337
|
-
function promoteProject2(cfg, credential, projectId) {
|
|
31585
|
+
function promoteProject2(cfg, credential, projectId, subproject) {
|
|
30338
31586
|
return lifecycleAction(cfg, credential, projectId, {
|
|
30339
31587
|
action: "project:promote",
|
|
30340
31588
|
verb: "Promote",
|
|
30341
31589
|
noun: "promotion",
|
|
31590
|
+
subproject,
|
|
30342
31591
|
execute: () => {
|
|
30343
|
-
const promo = executeApprovedPromote(cfg, projectId);
|
|
31592
|
+
const promo = executeApprovedPromote(cfg, projectId, subproject);
|
|
30344
31593
|
return {
|
|
30345
31594
|
status: "completed",
|
|
30346
31595
|
action: "project:promote",
|
|
30347
|
-
summary: `Promotion of project "${projectId}": ${promo.status} \u2014 ${promo.message}`,
|
|
31596
|
+
summary: `Promotion of project "${projectId}"${subprojectSuffix(subproject)}: ${promo.status} \u2014 ${promo.message}`,
|
|
30348
31597
|
promote: promo
|
|
30349
31598
|
};
|
|
30350
31599
|
}
|
|
30351
31600
|
});
|
|
30352
31601
|
}
|
|
31602
|
+
function subprojectSuffix(subproject) {
|
|
31603
|
+
return subproject ? ` subproject "${subproject}"` : "";
|
|
31604
|
+
}
|
|
31605
|
+
var SUBPROJECT_SCOPE_PAYLOAD = "SubprojectScope";
|
|
31606
|
+
function subprojectScopePayload(subproject) {
|
|
31607
|
+
if (!subproject) return {};
|
|
31608
|
+
return { payloadType: SUBPROJECT_SCOPE_PAYLOAD, payload: JSON.stringify({ subproject }) };
|
|
31609
|
+
}
|
|
31610
|
+
function readSubprojectScope(req) {
|
|
31611
|
+
if (req.payloadType !== SUBPROJECT_SCOPE_PAYLOAD || !req.payload) return void 0;
|
|
31612
|
+
const parsed = JSON.parse(req.payload);
|
|
31613
|
+
if (typeof parsed.subproject !== "string" || !parsed.subproject) {
|
|
31614
|
+
throw new Error(
|
|
31615
|
+
`Approved ${req.kind} request "${req.id}" carries a ${SUBPROJECT_SCOPE_PAYLOAD} payload with no usable subproject qualifier \u2014 refusing to execute, because falling back to the whole project would widen the scope the requester was confined to.`
|
|
31616
|
+
);
|
|
31617
|
+
}
|
|
31618
|
+
return parsed.subproject;
|
|
31619
|
+
}
|
|
30353
31620
|
function lifecycleAction(cfg, credential, projectId, opts) {
|
|
30354
31621
|
const principal = requirePrincipal7(cfg, credential);
|
|
30355
31622
|
const effective = authorize(cfg.dataDir, principal, PROJECT_WRITE_CAPABILITY4, "project", projectId);
|
|
@@ -30375,8 +31642,9 @@ function lifecycleAction(cfg, credential, projectId, opts) {
|
|
|
30375
31642
|
const pending = buildPendingRequest(
|
|
30376
31643
|
principal,
|
|
30377
31644
|
opts.action,
|
|
30378
|
-
`${opts.verb} project ${projectId}`,
|
|
30379
|
-
projectId
|
|
31645
|
+
`${opts.verb} project ${projectId}${subprojectSuffix(opts.subproject)}`,
|
|
31646
|
+
projectId,
|
|
31647
|
+
subprojectScopePayload(opts.subproject)
|
|
30380
31648
|
);
|
|
30381
31649
|
return createPendingOutcome(cfg, principal, pending, opts.action);
|
|
30382
31650
|
}
|
|
@@ -30450,12 +31718,14 @@ function executeApproved(cfg, req) {
|
|
|
30450
31718
|
return `Initialized project "${rec.id}" under the active pack policy.`;
|
|
30451
31719
|
}
|
|
30452
31720
|
case "project:lock": {
|
|
30453
|
-
const
|
|
30454
|
-
|
|
31721
|
+
const scope = readSubprojectScope(req);
|
|
31722
|
+
const lock = executeApprovedLock(cfg, req.projectId ?? "", scope);
|
|
31723
|
+
return `Locked project "${req.projectId}"${subprojectSuffix(scope)} (status: ${lock.status}).`;
|
|
30455
31724
|
}
|
|
30456
31725
|
case "project:promote": {
|
|
30457
|
-
const
|
|
30458
|
-
|
|
31726
|
+
const scope = readSubprojectScope(req);
|
|
31727
|
+
const promo = executeApprovedPromote(cfg, req.projectId ?? "", scope);
|
|
31728
|
+
return `Promotion of project "${req.projectId}"${subprojectSuffix(scope)}: ${promo.status} \u2014 ${promo.message}`;
|
|
30459
31729
|
}
|
|
30460
31730
|
default:
|
|
30461
31731
|
throw new Error(`Unsupported approval kind "${req.kind}".`);
|
|
@@ -31159,6 +32429,9 @@ function getProjectConfig2(cfg, credential, projectId) {
|
|
|
31159
32429
|
function setProjectType2(cfg, credential, projectId, projectType) {
|
|
31160
32430
|
return setProjectType(cfg, credential, projectId, projectType);
|
|
31161
32431
|
}
|
|
32432
|
+
function listProjectProfiles2(cfg, credential, project2) {
|
|
32433
|
+
return listProjectProfiles(cfg, credential, project2);
|
|
32434
|
+
}
|
|
31162
32435
|
function listProducers2(cfg, credential, project2) {
|
|
31163
32436
|
return listProducers(cfg, credential, project2);
|
|
31164
32437
|
}
|
|
@@ -31687,6 +32960,19 @@ var ShareSnapshotRegistry = class {
|
|
|
31687
32960
|
return stored;
|
|
31688
32961
|
}
|
|
31689
32962
|
};
|
|
32963
|
+
function selectCapturedOpenApi(snap, portalId) {
|
|
32964
|
+
const specs = snap.openapiSet;
|
|
32965
|
+
if (!specs || specs.length === 0) {
|
|
32966
|
+
if (portalId) return null;
|
|
32967
|
+
return snap.openapi ?? null;
|
|
32968
|
+
}
|
|
32969
|
+
if (portalId) {
|
|
32970
|
+
const hit = specs.find((s) => s.portalId === portalId);
|
|
32971
|
+
return hit ? hit.document : null;
|
|
32972
|
+
}
|
|
32973
|
+
if (specs.length === 1) return specs[0].document;
|
|
32974
|
+
return openApiIndexDocument(specs);
|
|
32975
|
+
}
|
|
31690
32976
|
var ShareSnapshotIndex = class {
|
|
31691
32977
|
constructor(store) {
|
|
31692
32978
|
this.store = store;
|
|
@@ -31694,12 +32980,12 @@ var ShareSnapshotIndex = class {
|
|
|
31694
32980
|
get(snapshotId) {
|
|
31695
32981
|
return this.store.read(snapshotId);
|
|
31696
32982
|
}
|
|
31697
|
-
getArtifact(snapshotId, kind) {
|
|
32983
|
+
getArtifact(snapshotId, kind, portalId) {
|
|
31698
32984
|
const snap = this.store.read(snapshotId);
|
|
31699
32985
|
if (!snap) return null;
|
|
31700
32986
|
if (kind === "canvas") return snap.canvasModel ?? null;
|
|
31701
32987
|
if (kind === "html") return snap.html ?? null;
|
|
31702
|
-
if (kind === "openapi") return snap
|
|
32988
|
+
if (kind === "openapi") return selectCapturedOpenApi(snap, portalId);
|
|
31703
32989
|
return null;
|
|
31704
32990
|
}
|
|
31705
32991
|
};
|
|
@@ -31709,8 +32995,8 @@ function putSnapshot(dataDir, snapshot) {
|
|
|
31709
32995
|
function getSnapshot2(dataDir, snapshotId) {
|
|
31710
32996
|
return new ShareSnapshotIndex(new ShareSnapshotStore(dataDir)).get(snapshotId);
|
|
31711
32997
|
}
|
|
31712
|
-
function getSnapshotArtifact(dataDir, snapshotId, kind) {
|
|
31713
|
-
return new ShareSnapshotIndex(new ShareSnapshotStore(dataDir)).getArtifact(snapshotId, kind);
|
|
32998
|
+
function getSnapshotArtifact(dataDir, snapshotId, kind, portalId) {
|
|
32999
|
+
return new ShareSnapshotIndex(new ShareSnapshotStore(dataDir)).getArtifact(snapshotId, kind, portalId);
|
|
31714
33000
|
}
|
|
31715
33001
|
function captureSnapshot(dataDir, principal, projectId, view, artifacts) {
|
|
31716
33002
|
const root = resolveProjectRoot(dataDir, principal, projectId);
|
|
@@ -31729,7 +33015,11 @@ function captureSnapshot(dataDir, principal, projectId, view, artifacts) {
|
|
|
31729
33015
|
}
|
|
31730
33016
|
if (artifacts.includes("openapi")) {
|
|
31731
33017
|
const result = hostSurfaces.exportBoundSurface("project", "openapi");
|
|
31732
|
-
|
|
33018
|
+
const specs = result.renderedSet ?? [];
|
|
33019
|
+
if (specs.length) {
|
|
33020
|
+
snapshot.openapiSet = specs;
|
|
33021
|
+
if (specs.length === 1) snapshot.openapi = specs[0].document;
|
|
33022
|
+
}
|
|
31733
33023
|
}
|
|
31734
33024
|
return snapshot;
|
|
31735
33025
|
});
|
|
@@ -32290,10 +33580,10 @@ function getWebProjectOpenApi(cfg, sessionId, projectId, portalId) {
|
|
|
32290
33580
|
if (!root) throw new ForbiddenError("project not authorized or unknown");
|
|
32291
33581
|
const result = runWithProjectRoot(root, () => hostSurfaces.exportBoundSurface("project", "openapi"));
|
|
32292
33582
|
const specs = result.renderedSet ?? (result.rendered ? [{ portalId: "", name: projectId, document: result.rendered }] : []);
|
|
32293
|
-
if (specs.length === 0) return
|
|
33583
|
+
if (specs.length === 0) return openApiIndexPage(projectId, []);
|
|
32294
33584
|
if (portalId) {
|
|
32295
|
-
const sel = specs.find((s) => s.portalId === portalId)
|
|
32296
|
-
return swaggerUiPage(sel.document, sel.name);
|
|
33585
|
+
const sel = specs.find((s) => s.portalId === portalId);
|
|
33586
|
+
return sel ? swaggerUiPage(sel.document, sel.name) : openApiIndexPage(projectId, specs);
|
|
32297
33587
|
}
|
|
32298
33588
|
if (specs.length === 1) return swaggerUiPage(specs[0].document, specs[0].name);
|
|
32299
33589
|
return openApiIndexPage(projectId, specs);
|
|
@@ -32301,7 +33591,7 @@ function getWebProjectOpenApi(cfg, sessionId, projectId, portalId) {
|
|
|
32301
33591
|
function openApiIndexPage(projectId, specs) {
|
|
32302
33592
|
const esc2 = (s) => s.replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c]);
|
|
32303
33593
|
const items = specs.map((s) => `<li><a href="/web/openapi?projectId=${encodeURIComponent(projectId)}&spec=${encodeURIComponent(s.portalId)}">${esc2(s.name)}</a> <code>${esc2(s.portalId)}</code></li>`).join("");
|
|
32304
|
-
return `<!doctype html><html><head><meta charset="utf-8"><title>${esc2(projectId)} \u2014 API specs</title><style>body{font-family:system-ui,sans-serif;max-width:680px;margin:48px auto;padding:0 20px;color:#e6e6e6;background:#161616}h1{font-size:20px}a{color:#6ea8fe;text-decoration:none}a:hover{text-decoration:underline}li{margin:10px 0}code{color:#8a94a6;font-size:12px;margin-left:8px}</style></head><body><h1>${esc2(projectId)} \u2014 API specs</h1
|
|
33594
|
+
return `<!doctype html><html><head><meta charset="utf-8"><title>${esc2(projectId)} \u2014 API specs</title><style>body{font-family:system-ui,sans-serif;max-width:680px;margin:48px auto;padding:0 20px;color:#e6e6e6;background:#161616}h1{font-size:20px}a{color:#6ea8fe;text-decoration:none}a:hover{text-decoration:underline}li{margin:10px 0}code{color:#8a94a6;font-size:12px;margin-left:8px}</style></head><body><h1>${esc2(projectId)} \u2014 API specs</h1>` + (specs.length ? `<p>This project exposes ${specs.length} separate public APIs, each with its own OpenAPI document and auth:</p><ul>${items}</ul>` : `<p>This project publishes no HTTP API \u2014 no public portal exposes one.</p>`) + `</body></html>`;
|
|
32305
33595
|
}
|
|
32306
33596
|
function reshapeLandscapeGraph(model, level) {
|
|
32307
33597
|
const unitNodeIds = new Set(model.nodes.filter((n) => n.nodeKind === "orgUnit").map((n) => n.id));
|
|
@@ -32335,7 +33625,31 @@ function reshapeLandscapeGraph(model, level) {
|
|
|
32335
33625
|
}
|
|
32336
33626
|
const kept = nodes.filter((n) => n.level <= level);
|
|
32337
33627
|
const keptIds = new Set(kept.map((n) => n.id));
|
|
32338
|
-
const
|
|
33628
|
+
const projectNodeIdByProject = /* @__PURE__ */ new Map();
|
|
33629
|
+
const interfaceOwnerProject = /* @__PURE__ */ new Map();
|
|
33630
|
+
for (const n of model.nodes) {
|
|
33631
|
+
if (n.projectId === void 0) continue;
|
|
33632
|
+
if (n.nodeKind === "project") projectNodeIdByProject.set(n.projectId, n.id);
|
|
33633
|
+
else if (n.nodeKind === "publicInterface") interfaceOwnerProject.set(n.id, n.projectId);
|
|
33634
|
+
}
|
|
33635
|
+
const retarget = (endpoint) => {
|
|
33636
|
+
if (keptIds.has(endpoint)) return endpoint;
|
|
33637
|
+
const owner = interfaceOwnerProject.get(endpoint);
|
|
33638
|
+
const projectNode = owner === void 0 ? void 0 : projectNodeIdByProject.get(owner);
|
|
33639
|
+
return projectNode !== void 0 && keptIds.has(projectNode) ? projectNode : void 0;
|
|
33640
|
+
};
|
|
33641
|
+
const edges = [];
|
|
33642
|
+
for (const e of model.edges) {
|
|
33643
|
+
if (keptIds.has(e.from) && keptIds.has(e.to)) {
|
|
33644
|
+
edges.push(e);
|
|
33645
|
+
continue;
|
|
33646
|
+
}
|
|
33647
|
+
if (e.relationId === void 0) continue;
|
|
33648
|
+
const from = retarget(e.from);
|
|
33649
|
+
const to = retarget(e.to);
|
|
33650
|
+
if (from === void 0 || to === void 0 || from === to) continue;
|
|
33651
|
+
edges.push({ ...e, from, to });
|
|
33652
|
+
}
|
|
32339
33653
|
return {
|
|
32340
33654
|
tier: "landscape",
|
|
32341
33655
|
nodes: kept,
|
|
@@ -34479,6 +35793,9 @@ function opsGetProjectConfig(cfg, sessionId, url, res) {
|
|
|
34479
35793
|
function opsSetProjectConfig(cfg, sessionId, body, res) {
|
|
34480
35794
|
sendJson(res, 200, setProjectType2(cfg, sessionId, String(body?.projectId ?? ""), String(body?.projectType ?? "")));
|
|
34481
35795
|
}
|
|
35796
|
+
function opsListProjectProfiles(cfg, sessionId, url, res) {
|
|
35797
|
+
sendJson(res, 200, { profiles: listProjectProfiles2(cfg, sessionId, q(url, "projectId") ?? "") });
|
|
35798
|
+
}
|
|
34482
35799
|
function opsListProducers(cfg, sessionId, url, res) {
|
|
34483
35800
|
sendJson(res, 200, { producers: listProducers2(cfg, sessionId, q(url, "projectId") ?? "") });
|
|
34484
35801
|
}
|
|
@@ -34718,6 +36035,9 @@ async function handleWebRequest(cfg, req, res, body, url, ctx) {
|
|
|
34718
36035
|
if (req.method === "POST" && parts.length === 3 && parts[2] === "config") {
|
|
34719
36036
|
return opsSetProjectConfig(cfg, sessionId, body, res);
|
|
34720
36037
|
}
|
|
36038
|
+
if (req.method === "GET" && parts.length === 3 && parts[2] === "profiles") {
|
|
36039
|
+
return opsListProjectProfiles(cfg, sessionId, url, res);
|
|
36040
|
+
}
|
|
34721
36041
|
if (req.method === "GET" && parts.length === 3 && parts[2] === "policy") {
|
|
34722
36042
|
return opsPolicyEvaluate(cfg, sessionId, url, res);
|
|
34723
36043
|
}
|
|
@@ -34942,8 +36262,8 @@ var RealtimeHub = class {
|
|
|
34942
36262
|
* complete the handshake, and register the connection. A bad path or session
|
|
34943
36263
|
* destroys the socket. */
|
|
34944
36264
|
handleUpgrade(cfg, req, socket) {
|
|
34945
|
-
const
|
|
34946
|
-
if (
|
|
36265
|
+
const path61 = (req.url ?? "/").split("?")[0];
|
|
36266
|
+
if (path61 !== REALTIME_PATH || !isWebSocketUpgrade(req)) {
|
|
34947
36267
|
socket.destroy();
|
|
34948
36268
|
return;
|
|
34949
36269
|
}
|
|
@@ -35088,7 +36408,7 @@ function deriveMcpOutcome(response) {
|
|
|
35088
36408
|
if (r.result && typeof r.result === "object" && r.result.isError) return "failed";
|
|
35089
36409
|
return "success";
|
|
35090
36410
|
}
|
|
35091
|
-
function auditToolCall(dataDir, principal, projectId, body, outcome) {
|
|
36411
|
+
function auditToolCall(dataDir, principal, projectId, body, outcome, subproject) {
|
|
35092
36412
|
const target = mcpToolTarget(body);
|
|
35093
36413
|
if (!target) return;
|
|
35094
36414
|
const actor = principal.subject ?? {
|
|
@@ -35106,7 +36426,8 @@ function auditToolCall(dataDir, principal, projectId, body, outcome) {
|
|
|
35106
36426
|
actor,
|
|
35107
36427
|
tokenId: principal.tokenId,
|
|
35108
36428
|
projectId,
|
|
35109
|
-
target
|
|
36429
|
+
target,
|
|
36430
|
+
...subproject ? { metadata: JSON.stringify({ subproject }) } : {}
|
|
35110
36431
|
};
|
|
35111
36432
|
try {
|
|
35112
36433
|
appendAuditEvent(dataDir, event, DEFAULT_AUDIT_POLICY);
|
|
@@ -35137,6 +36458,12 @@ var PROJECT_OPS_TOOLS = /* @__PURE__ */ new Set([
|
|
|
35137
36458
|
"sdd_host_produce",
|
|
35138
36459
|
"sdd_host_commit_project"
|
|
35139
36460
|
]);
|
|
36461
|
+
var PROJECT_RECORD_TOOLS = /* @__PURE__ */ new Set([
|
|
36462
|
+
"sdd_host_initialize_project",
|
|
36463
|
+
"sdd_host_get_approval_status",
|
|
36464
|
+
"sdd_host_await_approval",
|
|
36465
|
+
...PROJECT_OPS_TOOLS
|
|
36466
|
+
]);
|
|
35140
36467
|
function jsonRpcRequests(body) {
|
|
35141
36468
|
const arr = Array.isArray(body) ? body : [body];
|
|
35142
36469
|
return arr.filter((m) => !!m && typeof m === "object" && "method" in m);
|
|
@@ -35202,7 +36529,27 @@ function dataPlanePermissionError(cfg, principal, projectId, body) {
|
|
|
35202
36529
|
}
|
|
35203
36530
|
};
|
|
35204
36531
|
}
|
|
35205
|
-
|
|
36532
|
+
function subprojectConfinementError(projectId, subproject, body) {
|
|
36533
|
+
if (!subproject) return void 0;
|
|
36534
|
+
const msg = jsonRpcRequest(body);
|
|
36535
|
+
if (!msg || msg.method !== "tools/call") return void 0;
|
|
36536
|
+
const name = msg.params?.name;
|
|
36537
|
+
if (typeof name !== "string" || !PROJECT_RECORD_TOOLS.has(name)) return void 0;
|
|
36538
|
+
return {
|
|
36539
|
+
jsonrpc: "2.0",
|
|
36540
|
+
id: msg.id ?? null,
|
|
36541
|
+
result: {
|
|
36542
|
+
content: [
|
|
36543
|
+
{
|
|
36544
|
+
type: "text",
|
|
36545
|
+
text: `Refused \u2014 ${name} acts on the whole project "${projectId}", but this credential is bound to subproject "${projectId}${SUBPROJECT_SEPARATOR}${subproject}". An unqualified credential for "${projectId}" is required to call ${name}.`
|
|
36546
|
+
}
|
|
36547
|
+
],
|
|
36548
|
+
isError: true
|
|
36549
|
+
}
|
|
36550
|
+
};
|
|
36551
|
+
}
|
|
36552
|
+
async function dispatchProjectLifecycleTool(cfg, credential, projectId, body, subproject) {
|
|
35206
36553
|
const msg = jsonRpcRequest(body);
|
|
35207
36554
|
if (!msg || msg.method !== "tools/call") return void 0;
|
|
35208
36555
|
const name = msg.params?.name;
|
|
@@ -35219,10 +36566,10 @@ async function dispatchProjectLifecycleTool(cfg, credential, projectId, body) {
|
|
|
35219
36566
|
value = initializeProject(cfg, credential, args);
|
|
35220
36567
|
break;
|
|
35221
36568
|
case "sdd_host_lock_project":
|
|
35222
|
-
value = lockProject2(cfg, credential, projectId);
|
|
36569
|
+
value = lockProject2(cfg, credential, projectId, subproject);
|
|
35223
36570
|
break;
|
|
35224
36571
|
case "sdd_host_promote_project":
|
|
35225
|
-
value = promoteProject2(cfg, credential, projectId);
|
|
36572
|
+
value = promoteProject2(cfg, credential, projectId, subproject);
|
|
35226
36573
|
break;
|
|
35227
36574
|
case "sdd_host_await_approval":
|
|
35228
36575
|
value = await awaitApproval(
|
|
@@ -35310,8 +36657,8 @@ async function handleMcpRequest(cfg, req, res, body, credential) {
|
|
|
35310
36657
|
permissionSubject: { subjectId: "anonymous", roleBindings: [], instanceAdmin: true }
|
|
35311
36658
|
};
|
|
35312
36659
|
}
|
|
35313
|
-
const
|
|
35314
|
-
if (!
|
|
36660
|
+
const binding = resolveProjectBinding(cfg.dataDir, principal, projectSelector(req));
|
|
36661
|
+
if (!binding) {
|
|
35315
36662
|
sendJson(res, 403, { error: "project not authorized, unknown, or not specified" });
|
|
35316
36663
|
return;
|
|
35317
36664
|
}
|
|
@@ -35326,19 +36673,26 @@ async function handleMcpRequest(cfg, req, res, body, credential) {
|
|
|
35326
36673
|
});
|
|
35327
36674
|
return;
|
|
35328
36675
|
}
|
|
35329
|
-
await runWithProjectRoot(
|
|
35330
|
-
const projectId =
|
|
35331
|
-
const
|
|
36676
|
+
await runWithProjectRoot(binding.rootPath, async () => {
|
|
36677
|
+
const projectId = binding.projectId;
|
|
36678
|
+
const subproject = binding.subproject;
|
|
36679
|
+
const confinementError = subprojectConfinementError(projectId, subproject, body);
|
|
36680
|
+
if (confinementError !== void 0) {
|
|
36681
|
+
sendJson(res, 200, confinementError);
|
|
36682
|
+
auditToolCall(cfg.dataDir, principal, projectId, body, deriveMcpOutcome(confinementError), subproject);
|
|
36683
|
+
return;
|
|
36684
|
+
}
|
|
36685
|
+
const dispatchedResponse = await dispatchProjectLifecycleTool(cfg, cred, projectId, body, subproject);
|
|
35332
36686
|
if (dispatchedResponse !== void 0) {
|
|
35333
36687
|
sendJson(res, 200, dispatchedResponse);
|
|
35334
|
-
auditToolCall(cfg.dataDir, principal, projectId, body, deriveMcpOutcome(dispatchedResponse));
|
|
36688
|
+
auditToolCall(cfg.dataDir, principal, projectId, body, deriveMcpOutcome(dispatchedResponse), subproject);
|
|
35335
36689
|
for (const ch of mcpChangeChannels(body, projectId, dispatchedResponse)) publishChange(ch);
|
|
35336
36690
|
return;
|
|
35337
36691
|
}
|
|
35338
36692
|
const permissionError = dataPlanePermissionError(cfg, principal, projectId, body);
|
|
35339
36693
|
if (permissionError !== void 0) {
|
|
35340
36694
|
sendJson(res, 200, permissionError);
|
|
35341
|
-
auditToolCall(cfg.dataDir, principal, projectId, body, deriveMcpOutcome(permissionError));
|
|
36695
|
+
auditToolCall(cfg.dataDir, principal, projectId, body, deriveMcpOutcome(permissionError), subproject);
|
|
35342
36696
|
return;
|
|
35343
36697
|
}
|
|
35344
36698
|
const server = createScopedServer();
|
|
@@ -35359,7 +36713,7 @@ async function handleMcpRequest(cfg, req, res, body, credential) {
|
|
|
35359
36713
|
};
|
|
35360
36714
|
await server.connect(transport);
|
|
35361
36715
|
await transport.handleRequest(req, res, body);
|
|
35362
|
-
auditToolCall(cfg.dataDir, principal, projectId, body, deriveMcpOutcome(response));
|
|
36716
|
+
auditToolCall(cfg.dataDir, principal, projectId, body, deriveMcpOutcome(response), subproject);
|
|
35363
36717
|
for (const ch of mcpChangeChannels(body, projectId, response)) publishChange(ch);
|
|
35364
36718
|
});
|
|
35365
36719
|
}
|
|
@@ -35427,7 +36781,7 @@ var CONTENT_TYPE = {
|
|
|
35427
36781
|
openapi: "application/json",
|
|
35428
36782
|
canvas: "application/json"
|
|
35429
36783
|
};
|
|
35430
|
-
function downloadArtifact(cfg, token, kind, meta) {
|
|
36784
|
+
function downloadArtifact(cfg, token, kind, meta, portalId) {
|
|
35431
36785
|
const link = linkByTokenHash(cfg.dataDir, hashToken(token));
|
|
35432
36786
|
const check = usable(link);
|
|
35433
36787
|
if ("outcome" in check) {
|
|
@@ -35439,7 +36793,7 @@ function downloadArtifact(cfg, token, kind, meta) {
|
|
|
35439
36793
|
record(cfg.dataDir, check.link.id, meta, "denied-download");
|
|
35440
36794
|
return { found: false, outcome: "denied-download" };
|
|
35441
36795
|
}
|
|
35442
|
-
const content = getSnapshotArtifact(cfg.dataDir, check.link.snapshotId, kind);
|
|
36796
|
+
const content = getSnapshotArtifact(cfg.dataDir, check.link.snapshotId, kind, portalId);
|
|
35443
36797
|
if (content === null) {
|
|
35444
36798
|
record(cfg.dataDir, check.link.id, meta, "not-found");
|
|
35445
36799
|
return { found: false, outcome: "not-found" };
|
|
@@ -35449,6 +36803,35 @@ function downloadArtifact(cfg, token, kind, meta) {
|
|
|
35449
36803
|
}
|
|
35450
36804
|
|
|
35451
36805
|
// src/server/sharehttp.ts
|
|
36806
|
+
init_filenames();
|
|
36807
|
+
function specParam(req) {
|
|
36808
|
+
const url = req.url ?? "";
|
|
36809
|
+
const q2 = url.indexOf("?");
|
|
36810
|
+
if (q2 < 0) return void 0;
|
|
36811
|
+
return new URLSearchParams(url.slice(q2 + 1)).get("spec") || void 0;
|
|
36812
|
+
}
|
|
36813
|
+
function asOpenApiIndex(payload) {
|
|
36814
|
+
try {
|
|
36815
|
+
const parsed = JSON.parse(payload);
|
|
36816
|
+
return parsed && parsed.openapiIndex === true && Array.isArray(parsed.specs) ? parsed : null;
|
|
36817
|
+
} catch {
|
|
36818
|
+
return null;
|
|
36819
|
+
}
|
|
36820
|
+
}
|
|
36821
|
+
function escapeHtml2(s) {
|
|
36822
|
+
return s.replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c]);
|
|
36823
|
+
}
|
|
36824
|
+
function sharedOpenApiIndexPage(index) {
|
|
36825
|
+
const items = index.specs.map(
|
|
36826
|
+
(s) => `<li><a href="?spec=${encodeURIComponent(s.portalId)}">${escapeHtml2(s.name)}</a><code>${escapeHtml2(s.portalId)}</code></li>`
|
|
36827
|
+
).join("");
|
|
36828
|
+
return `<!doctype html><html><head><meta charset="utf-8"><title>Shared APIs</title><meta name="viewport" content="width=device-width, initial-scale=1"><style>body{font:15px/1.6 system-ui,sans-serif;max-width:680px;margin:48px auto;padding:0 20px;background:#0b1120;color:#e8e8f0}h1{font-size:20px}a{color:#6ea8fe;text-decoration:none}a:hover{text-decoration:underline}li{margin:10px 0}code{color:#9a9aab;font-size:12px;margin-left:8px}p{color:#9a9aab}</style></head><body><h1>Shared APIs</h1><p>This share exposes ${index.specs.length} separate APIs, each with its own OpenAPI document and auth:</p><ul>${items}</ul></body></html>`;
|
|
36829
|
+
}
|
|
36830
|
+
function downloadFilename(kind, portalId, payload) {
|
|
36831
|
+
if (kind !== "openapi") return `shared-canvas.${safeFilenamePart(kind)}`;
|
|
36832
|
+
if (portalId) return `shared-canvas.${safeFilenamePart(portalId)}.openapi.json`;
|
|
36833
|
+
return asOpenApiIndex(payload) ? "shared-canvas.openapi.index.json" : "shared-canvas.openapi.json";
|
|
36834
|
+
}
|
|
35452
36835
|
function shareRequestMeta(req) {
|
|
35453
36836
|
const fwd = req.headers["x-forwarded-for"];
|
|
35454
36837
|
const ip = (Array.isArray(fwd) ? fwd[0] : fwd)?.split(",")[0].trim() || req.socket?.remoteAddress || "unknown";
|
|
@@ -35500,14 +36883,16 @@ function serveSharedModel(cfg, token, req, res) {
|
|
|
35500
36883
|
);
|
|
35501
36884
|
}
|
|
35502
36885
|
function serveSharedOpenApi(cfg, token, req, res) {
|
|
35503
|
-
const result = downloadArtifact(cfg, token, "openapi", shareRequestMeta(req));
|
|
36886
|
+
const result = downloadArtifact(cfg, token, "openapi", shareRequestMeta(req), specParam(req));
|
|
35504
36887
|
if (!result.found || result.content === void 0) return notFoundPage(res);
|
|
36888
|
+
const index = asOpenApiIndex(result.content);
|
|
35505
36889
|
harden(res, void 0, "text/html; charset=utf-8");
|
|
35506
36890
|
res.statusCode = 200;
|
|
35507
|
-
res.end(swaggerUiPage(result.content, "Shared API"));
|
|
36891
|
+
res.end(index ? sharedOpenApiIndexPage(index) : swaggerUiPage(result.content, "Shared API"));
|
|
35508
36892
|
}
|
|
35509
36893
|
function serveSharedDownload(cfg, token, kind, req, res) {
|
|
35510
|
-
const
|
|
36894
|
+
const portalId = specParam(req);
|
|
36895
|
+
const result = downloadArtifact(cfg, token, kind, shareRequestMeta(req), portalId);
|
|
35511
36896
|
if (!result.found || result.content === void 0) {
|
|
35512
36897
|
if (result.outcome === "denied-download") {
|
|
35513
36898
|
harden(res, void 0, "text/plain");
|
|
@@ -35517,9 +36902,11 @@ function serveSharedDownload(cfg, token, kind, req, res) {
|
|
|
35517
36902
|
}
|
|
35518
36903
|
return notFoundPage(res);
|
|
35519
36904
|
}
|
|
35520
|
-
const ext = kind === "html" ? "html" : kind === "openapi" ? "openapi.json" : kind;
|
|
35521
36905
|
harden(res, void 0, result.contentType ?? "application/octet-stream");
|
|
35522
|
-
res.setHeader(
|
|
36906
|
+
res.setHeader(
|
|
36907
|
+
"content-disposition",
|
|
36908
|
+
`attachment; filename="${downloadFilename(kind, portalId, result.content)}"`
|
|
36909
|
+
);
|
|
35523
36910
|
res.statusCode = 200;
|
|
35524
36911
|
res.end(result.content);
|
|
35525
36912
|
}
|
|
@@ -35761,7 +37148,7 @@ function routeData(cfg, req, res) {
|
|
|
35761
37148
|
}
|
|
35762
37149
|
function readExposurePolicyFile(dataDir) {
|
|
35763
37150
|
try {
|
|
35764
|
-
const raw = fs49.readFileSync(
|
|
37151
|
+
const raw = fs49.readFileSync(path58.join(dataDir, "exposure-policy.json"), "utf8");
|
|
35765
37152
|
const parsed = JSON.parse(raw);
|
|
35766
37153
|
return parsed && typeof parsed === "object" ? parsed : void 0;
|
|
35767
37154
|
} catch {
|
|
@@ -36446,9 +37833,9 @@ function seedDemoTree() {
|
|
|
36446
37833
|
|
|
36447
37834
|
// src/commands/host.ts
|
|
36448
37835
|
function resolveHostConfig(options) {
|
|
36449
|
-
const dataDir = options.dataDir || process.env["WAIRON_DATA_DIR"] ||
|
|
37836
|
+
const dataDir = options.dataDir || process.env["WAIRON_DATA_DIR"] || path59.join(os10.homedir(), ".wairon", "data");
|
|
36450
37837
|
if (!process.env["WAIRON_PACKS_DIR"]) {
|
|
36451
|
-
process.env["WAIRON_PACKS_DIR"] =
|
|
37838
|
+
process.env["WAIRON_PACKS_DIR"] = path59.join(dataDir, "packs");
|
|
36452
37839
|
}
|
|
36453
37840
|
const cfg = {
|
|
36454
37841
|
host: options.host || "0.0.0.0",
|
|
@@ -36573,13 +37960,13 @@ function openBrowser(url) {
|
|
|
36573
37960
|
}
|
|
36574
37961
|
async function runDev(options = {}) {
|
|
36575
37962
|
const cwd = process.cwd();
|
|
36576
|
-
if (!fs50.existsSync(
|
|
37963
|
+
if (!fs50.existsSync(path59.join(cwd, ".wai"))) {
|
|
36577
37964
|
throw new WaironError(
|
|
36578
37965
|
"No .wai/ found in the current directory. Run `wairon dev` from a wairon project root (or run `wairon init` first)."
|
|
36579
37966
|
);
|
|
36580
37967
|
}
|
|
36581
37968
|
const hash = crypto20.createHash("sha256").update(cwd).digest("hex").slice(0, 16);
|
|
36582
|
-
const dataDir =
|
|
37969
|
+
const dataDir = path59.join(os10.tmpdir(), "wairon-dev", hash);
|
|
36583
37970
|
fs50.mkdirSync(dataDir, { recursive: true });
|
|
36584
37971
|
registerLocalDevProject(dataDir, "local", cwd);
|
|
36585
37972
|
const port = options.port ? Number(options.port) : 8080;
|
|
@@ -36988,8 +38375,8 @@ async function runHostPacks(action, options = {}) {
|
|
|
36988
38375
|
}
|
|
36989
38376
|
case "install": {
|
|
36990
38377
|
if (!options.file) throw new WaironError("`--file <path>` (a declarative pack YAML) is required for install.");
|
|
36991
|
-
const name = options.name ??
|
|
36992
|
-
const content = fs50.readFileSync(
|
|
38378
|
+
const name = options.name ?? path59.basename(options.file).replace(/\.(ya?ml)$/i, "");
|
|
38379
|
+
const content = fs50.readFileSync(path59.resolve(options.file), "utf8");
|
|
36993
38380
|
const desc = project2 ? installProjectPack(cfg, cred, project2, name, content) : installGlobalPack(cfg, cred, name, content);
|
|
36994
38381
|
logger.success(`Installed ${scope} pack "${desc.name}" (${desc.profiles} profile(s), ${desc.languages} language(s)).`);
|
|
36995
38382
|
if (project2) logger.info("Committed with the project \u2014 every clone and CI will enforce it.");
|
|
@@ -37129,15 +38516,29 @@ async function runSurface(action, options = {}) {
|
|
|
37129
38516
|
if (format !== "native" && format !== "openapi") {
|
|
37130
38517
|
throw new WaironError(`Unknown format "${format}" (supported: native, openapi).`);
|
|
37131
38518
|
}
|
|
37132
|
-
|
|
38519
|
+
if (options.portal && format !== "openapi") {
|
|
38520
|
+
throw new WaironError("`--portal` selects one OpenAPI document and only applies to `--format openapi`.");
|
|
38521
|
+
}
|
|
38522
|
+
const result = exportSurface(audience, format, options.out, options.portal);
|
|
37133
38523
|
logger.success(
|
|
37134
38524
|
`Projected surface of "${result.snapshot.projectName}": ${result.snapshot.interfaces.length} interface(s), ${result.snapshot.types.length} type(s) at audience \u2265 ${audience}.`
|
|
37135
38525
|
);
|
|
37136
|
-
|
|
37137
|
-
|
|
38526
|
+
const written = result.writtenPaths ?? (result.writtenTo ? [result.writtenTo] : []);
|
|
38527
|
+
if (written.length === 1) {
|
|
38528
|
+
logger.info(`Written to ${written[0]}`);
|
|
38529
|
+
} else if (written.length > 1) {
|
|
38530
|
+
logger.info(`Written ${written.length} document(s) \u2014 one per portal:`);
|
|
38531
|
+
for (const p of written) logger.info(` ${p}`);
|
|
37138
38532
|
} else if (result.rendered) {
|
|
37139
38533
|
process.stdout.write(`${result.rendered}
|
|
37140
38534
|
`);
|
|
38535
|
+
} else if (result.renderedSet && result.renderedSet.length > 1) {
|
|
38536
|
+
logger.info(
|
|
38537
|
+
`This project publishes ${result.renderedSet.length} portals \u2014 pick one with \`--portal <id>\` (or use --out to write them all):`
|
|
38538
|
+
);
|
|
38539
|
+
for (const spec of result.renderedSet) {
|
|
38540
|
+
logger.info(` ${import_chalk19.default.cyan(spec.portalId)} \u2014 ${spec.name}`);
|
|
38541
|
+
}
|
|
37141
38542
|
} else {
|
|
37142
38543
|
for (const entry of result.snapshot.interfaces) {
|
|
37143
38544
|
logger.info(` ${import_chalk19.default.cyan(entry.id)} (${entry.type}, ${entry.audience}) \u2014 ${entry.methods.length} method(s)`);
|
|
@@ -37181,13 +38582,27 @@ async function runSurface(action, options = {}) {
|
|
|
37181
38582
|
for (const p of written) logger.info(` ${p}`);
|
|
37182
38583
|
return;
|
|
37183
38584
|
}
|
|
38585
|
+
case "externals": {
|
|
38586
|
+
const entries = listExternalInterfaces();
|
|
38587
|
+
if (!entries.length) {
|
|
38588
|
+
logger.info("No external surfaces available (.wai/surfaces/ holds no snapshots).");
|
|
38589
|
+
return;
|
|
38590
|
+
}
|
|
38591
|
+
const freshness = (f) => f === "fresh" ? import_chalk19.default.green(f) : f === "stale" ? import_chalk19.default.yellow(f) : import_chalk19.default.gray(f);
|
|
38592
|
+
for (const e of entries) {
|
|
38593
|
+
logger.info(
|
|
38594
|
+
`${e.sourceKind.padEnd(8)} ${import_chalk19.default.cyan(e.projectName)} [${e.origin}] ${freshness(e.freshness)} \u2014 ${e.interfaceIds.length ? e.interfaceIds.join(", ") : "(no interfaces)"}`
|
|
38595
|
+
);
|
|
38596
|
+
}
|
|
38597
|
+
return;
|
|
38598
|
+
}
|
|
37184
38599
|
default:
|
|
37185
|
-
throw new WaironError(`Unknown surface action "${action}" (supported: export, import, list, generate-children).`);
|
|
38600
|
+
throw new WaironError(`Unknown surface action "${action}" (supported: export, import, list, generate-children, externals).`);
|
|
37186
38601
|
}
|
|
37187
38602
|
}
|
|
37188
38603
|
|
|
37189
38604
|
// src/commands/subsystem.ts
|
|
37190
|
-
var
|
|
38605
|
+
var path60 = __toESM(require("path"));
|
|
37191
38606
|
init_logger();
|
|
37192
38607
|
init_errors();
|
|
37193
38608
|
init_fs();
|
|
@@ -37223,9 +38638,9 @@ async function runSubsystemAdd(id, options = {}) {
|
|
|
37223
38638
|
updatedAt: now
|
|
37224
38639
|
};
|
|
37225
38640
|
createChainedSubsystem(subsystem, displayName);
|
|
37226
|
-
const childDir =
|
|
38641
|
+
const childDir = path60.resolve(getProjectRoot(), options.projectPath);
|
|
37227
38642
|
logger.success(`Added external subsystem "${id}" \u2192 ${options.projectPath}`);
|
|
37228
|
-
logger.info(`Scaffolded child project at ${
|
|
38643
|
+
logger.info(`Scaffolded child project at ${path60.relative(process.cwd(), childDir) || "."}`);
|
|
37229
38644
|
logger.info(`Design its spec tree from this parent using namespaced ids (e.g. ${id}::<component>).`);
|
|
37230
38645
|
}
|
|
37231
38646
|
async function runSubsystemMove(id, options = {}) {
|
|
@@ -37248,9 +38663,9 @@ async function runSubsystemExternalize(id, options = {}) {
|
|
|
37248
38663
|
throw new WaironError("--project-path (the subproject destination) is required.");
|
|
37249
38664
|
}
|
|
37250
38665
|
externalizeSubsystem(id, options.projectPath);
|
|
37251
|
-
const childDir =
|
|
38666
|
+
const childDir = path60.resolve(getProjectRoot(), options.projectPath);
|
|
37252
38667
|
logger.success(`Externalized subsystem "${id}" \u2192 ${options.projectPath}`);
|
|
37253
|
-
logger.info(`Moved its specs into ${
|
|
38668
|
+
logger.info(`Moved its specs into ${path60.relative(process.cwd(), childDir) || "."} (now a standalone subproject).`);
|
|
37254
38669
|
logger.info("Move the source code there yourself, then run `wairon validate` to confirm the tree.");
|
|
37255
38670
|
}
|
|
37256
38671
|
async function runSubsystemInternalize(id) {
|
|
@@ -37289,8 +38704,64 @@ program.command("generate").description("Generate agent output files from the sp
|
|
|
37289
38704
|
dryRun: opts.dryRun
|
|
37290
38705
|
});
|
|
37291
38706
|
});
|
|
38707
|
+
async function runLock2(options) {
|
|
38708
|
+
assertProjectInitialized();
|
|
38709
|
+
if (!pathExists(AI_PATHS.specsSystem())) {
|
|
38710
|
+
logger.error("No SDD spec tree found (.wai/specs). Nothing to lock.");
|
|
38711
|
+
process.exit(1);
|
|
38712
|
+
}
|
|
38713
|
+
const projectConfig = loadProjectConfig();
|
|
38714
|
+
logger.info("Analyzing and validating specifications in-memory...");
|
|
38715
|
+
const dry = validateAsComplete({
|
|
38716
|
+
rules: projectConfig.rules,
|
|
38717
|
+
projectType: projectConfig.projectType,
|
|
38718
|
+
scopeSubsystem: options.subsystem,
|
|
38719
|
+
recursive: options.recursive ?? true
|
|
38720
|
+
});
|
|
38721
|
+
const errors = dry.issues.filter((i) => i.severity === "error");
|
|
38722
|
+
if (errors.length > 0) {
|
|
38723
|
+
logger.header("Cannot lock \u2014 the spec tree does not validate as complete");
|
|
38724
|
+
let errorCount = 0;
|
|
38725
|
+
const MAX_PRINT = 100;
|
|
38726
|
+
let skippedErrors = 0;
|
|
38727
|
+
for (const i of errors) {
|
|
38728
|
+
if (errorCount < MAX_PRINT) {
|
|
38729
|
+
logger.error(`${i.specId ? `[${i.specId}] ` : ""}[${i.code}] ${i.message}`);
|
|
38730
|
+
errorCount++;
|
|
38731
|
+
} else {
|
|
38732
|
+
skippedErrors++;
|
|
38733
|
+
}
|
|
38734
|
+
}
|
|
38735
|
+
if (skippedErrors > 0) {
|
|
38736
|
+
logger.error(`... and ${skippedErrors} more error(s) omitted.`);
|
|
38737
|
+
}
|
|
38738
|
+
logger.blank();
|
|
38739
|
+
logger.info("Fix the errors above, then run `wairon lock` again. Nothing was changed.");
|
|
38740
|
+
process.exit(1);
|
|
38741
|
+
}
|
|
38742
|
+
logger.header("Lock SDD specs");
|
|
38743
|
+
const record2 = await runLock(options, dry);
|
|
38744
|
+
if (!record2) {
|
|
38745
|
+
logger.info("Cancelled. Nothing was changed.");
|
|
38746
|
+
return;
|
|
38747
|
+
}
|
|
38748
|
+
const childPaths = generateChildSnapshots();
|
|
38749
|
+
if (childPaths.length > 0) {
|
|
38750
|
+
logger.blank();
|
|
38751
|
+
logger.success(`Regenerated the family/sibling surfaces into ${childPaths.length} chained child snapshot(s):`);
|
|
38752
|
+
for (const p of childPaths) logger.info(` ${p}`);
|
|
38753
|
+
}
|
|
38754
|
+
logger.blank();
|
|
38755
|
+
await runGenerate({ domain: options.subsystem });
|
|
38756
|
+
logger.blank();
|
|
38757
|
+
logger.success("Specs locked and agent topology generated.");
|
|
38758
|
+
logger.info(`Lock record written (.wai/lock.json): stateId ${record2.stateId.algorithm}:${record2.stateId.digest} \u2014 status ${record2.status}.`);
|
|
38759
|
+
logger.warn(
|
|
38760
|
+
"Restart any running AI agent sessions (Claude Code / Antigravity / Codex) so the newly generated implementer agents load \u2014 they are not picked up mid-session."
|
|
38761
|
+
);
|
|
38762
|
+
}
|
|
37292
38763
|
program.command("lock").description("Final check before implementation: validate the spec tree as complete, freeze all specs to complete, and (re)generate the agent topology \u2014 only if it validates").option("-y, --yes", "skip the confirmation prompt (for scripts / CI)").option("--subsystem <id>", "only lock specs in the specified subsystem").option("--no-recursive", "do not recursively validate subprojects").action(async (opts) => {
|
|
37293
|
-
await
|
|
38764
|
+
await runLock2({ yes: opts.yes, subsystem: opts.subsystem, recursive: opts.recursive });
|
|
37294
38765
|
});
|
|
37295
38766
|
program.command("validate").description("Validate the project configuration and the SDD Spec Tree").option("--ci", "treat warnings as errors for CI pipelines").option("--subsystem <id>", "only validate the specified subsystem (granular)").option("--no-recursive", "do not recursively validate subprojects").action(async (opts) => {
|
|
37296
38767
|
await runValidate({ ci: opts.ci, subsystem: opts.subsystem, recursive: opts.recursive });
|
|
@@ -37406,13 +38877,14 @@ mcpCmd.command("status").description("Show whether the wairon MCP server is regi
|
|
|
37406
38877
|
program.command("produce <target>").description("project the local project's specs to a producer target (notion | miro)").option("--page <id>", "parent page/board id in the target").option("--token <token>", "integration token (else env, else interactive prompt)").action(async (target, opts) => {
|
|
37407
38878
|
await runProduce(target, { page: opts.page, token: opts.token });
|
|
37408
38879
|
});
|
|
37409
|
-
program.command("surface <action>").description("public surface exchange: export | import | list | generate-children").option("--audience <level>", "export ceiling: project | department | instance | partner | external (default instance)").option("--format <fmt>", "export format: native | openapi (default native)").option("--out <path>", "export output path (else print)").option("--source <path>", "import: the surface document (native snapshot YAML or OpenAPI)").option("--origin <origin>", "import provenance: exchanged | authored (default authored)").action(async (action, opts) => {
|
|
38880
|
+
program.command("surface <action>").description("public surface exchange: export | import | list | generate-children | externals").option("--audience <level>", "export ceiling: project | department | instance | partner | external (default instance)").option("--format <fmt>", "export format: native | openapi (default native)").option("--out <path>", "export output path (else print)").option("--portal <id>", "export: select one portal's OpenAPI spec (a multi-portal project renders one document per portal)").option("--source <path>", "import: the surface document (native snapshot YAML or OpenAPI)").option("--origin <origin>", "import provenance: exchanged | authored (default authored)").action(async (action, opts) => {
|
|
37410
38881
|
await runSurface(action, {
|
|
37411
38882
|
audience: opts.audience,
|
|
37412
38883
|
format: opts.format,
|
|
37413
38884
|
out: opts.out,
|
|
37414
38885
|
source: opts.source,
|
|
37415
|
-
origin: opts.origin
|
|
38886
|
+
origin: opts.origin,
|
|
38887
|
+
portal: opts.portal
|
|
37416
38888
|
});
|
|
37417
38889
|
});
|
|
37418
38890
|
program.command("serve").description("Run the wairon hosting server: HTTP MCP for many isolated projects + admin API").option("--host <host>", "data-plane bind host (default 0.0.0.0)").option("--port <port>", "data-plane port (default 8080)").option("--admin-host <host>", "admin-plane bind host (default 127.0.0.1)").option("--admin-port <port>", "admin-plane port (default 8081)").option("--data-dir <path>", "data root holding projects/ and auth/ (default WAIRON_DATA_DIR or ~/.wairon/data)").option("--no-auth", "disable data-plane auth (trusted networks only)").action(async (opts) => {
|