@wairon/cli 5.0.2-dev.9 → 5.1.1-dev.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +2260 -840
- package/dist/cli/index.js.map +1 -1
- package/dist/index.js +1078 -152
- package/dist/index.js.map +1 -1
- package/dist/templates/skills/sdd-architect.md +1 -1
- 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.
|
|
68
|
+
WAIRON_VERSION = "5.1.1-dev.1";
|
|
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.
|
|
@@ -1097,10 +1122,14 @@ var init_specs = __esm({
|
|
|
1097
1122
|
// Required if type is 'dispatch': the capability routed through the target Portal's dispatch table
|
|
1098
1123
|
/**
|
|
1099
1124
|
* call/dispatch only: the credential this step presents to an authed callee
|
|
1100
|
-
* Portal, and WHERE it is loaded from (`from`
|
|
1101
|
-
* `env:API_KEY`, a config key, a
|
|
1102
|
-
* never
|
|
1103
|
-
*
|
|
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.
|
|
1104
1133
|
*/
|
|
1105
1134
|
auth: import_zod6.z.object({ from: import_zod6.z.string(), note: import_zod6.z.string().optional() }).optional(),
|
|
1106
1135
|
assertsGuarantees: import_zod6.z.array(GuaranteeSchema).optional(),
|
|
@@ -1910,6 +1939,47 @@ var init_loader = __esm({
|
|
|
1910
1939
|
}
|
|
1911
1940
|
});
|
|
1912
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
|
+
|
|
1913
1983
|
// src/core/narrative-labels.ts
|
|
1914
1984
|
function resolveNarrativeLabels(methodName, steps) {
|
|
1915
1985
|
const errors = [];
|
|
@@ -2995,6 +3065,16 @@ header input[type="search"]::placeholder { color:var(--dim); }
|
|
|
2995
3065
|
#moreMenu .dropdown { display:block; width:100%; }
|
|
2996
3066
|
#moreMenu .dropdown > .tbtn, #moreMenu > .tbtn { display:block; width:100%; text-align:left; border:none; background:transparent; margin:2px 0; }
|
|
2997
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; }
|
|
2998
3078
|
|
|
2999
3079
|
/* Settings panel \u2014 toggle switches */
|
|
3000
3080
|
.settings-menu { min-width:266px; }
|
|
@@ -3118,9 +3198,17 @@ body.presentation #exitPresent, body.presentation #presentDetails { display:bloc
|
|
|
3118
3198
|
<button data-vm="types">Types</button>
|
|
3119
3199
|
<button data-vm="databases">Databases</button>
|
|
3120
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>
|
|
3121
3205
|
<nav id="crumbs"></nav>
|
|
3122
3206
|
<span class="divider"></span>
|
|
3123
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>
|
|
3124
3212
|
<div class="seg" id="typesDetailSeg" style="display:none" title="ERD detail level">
|
|
3125
3213
|
<button data-td="full">Full</button>
|
|
3126
3214
|
<button data-td="fields">Fields</button>
|
|
@@ -3637,13 +3725,300 @@ var MODEL = __MODEL_JSON__;
|
|
|
3637
3725
|
var BOX_W = 200, BOX_H = 56, SUBBOX_W = 230, SUBBOX_H = 84, GAP_X = 110, GAP_Y = 34;
|
|
3638
3726
|
var INNER_W = 130, INNER_H = 36, INNER_GAPX = 26, INNER_GAPY = 12, HEAD_H = 34, PADI = 14;
|
|
3639
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
|
+
|
|
3640
4010
|
// Micro-layout for a container's direct children when Internals is on:
|
|
3641
4011
|
// layered mini columns + intra-container edges. Each external relation gets
|
|
3642
4012
|
// its own small PORT node INSIDE the container (one per external
|
|
3643
4013
|
// counterpart; incoming left, outgoing right). Children connect to ports
|
|
3644
4014
|
// with short edges that never leave the box \u2014 the real cross-boundary line
|
|
3645
4015
|
// is only revealed on hover, or pinned while the port is selected.
|
|
3646
|
-
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
|
+
}
|
|
3647
4022
|
var kids = state.internals && entry.hasKids ? childrenOf({ kind: entry.kind, id: entry.id }) : [];
|
|
3648
4023
|
if (!kids.length) return null;
|
|
3649
4024
|
var scope = { kind: entry.kind, id: entry.id };
|
|
@@ -4215,12 +4590,42 @@ var MODEL = __MODEL_JSON__;
|
|
|
4215
4590
|
if (scope.kind === 'types' || scope.kind === 'databases') return buildTypeElements();
|
|
4216
4591
|
var entries = childrenOf(scope);
|
|
4217
4592
|
var eles = [];
|
|
4593
|
+
var deepCtx = buildDeepContext(scope, entries);
|
|
4218
4594
|
var ve = viewEdges(scope, entries);
|
|
4219
4595
|
// Data-coupling overlay: same scoping pipeline, a different edge source.
|
|
4220
4596
|
var vd = state.dataCoupling ? viewEdges(scope, entries, MODEL.dataEdges) : { agg: {}, ghosts: {} };
|
|
4221
4597
|
Object.keys(vd.ghosts).forEach(function (g) { if (!ve.ghosts[g]) ve.ghosts[g] = vd.ghosts[g]; });
|
|
4222
4598
|
var inners = {};
|
|
4223
|
-
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
|
+
}
|
|
4224
4629
|
|
|
4225
4630
|
// Resolve a port's reveal target(s) in THIS view. Preference order: the
|
|
4226
4631
|
// MATCHING PORT inside the counterpart's container (a port-to-port line
|
|
@@ -4352,17 +4757,25 @@ var MODEL = __MODEL_JSON__;
|
|
|
4352
4757
|
+ (state.showIssues && issuesBySpec[e.id] ? ' hasIssue' : '')
|
|
4353
4758
|
+ (state.selectedKind === e.kind && state.selected === e.id ? ' sel' : '');
|
|
4354
4759
|
if (inner) {
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
|
|
4358
|
-
|
|
4359
|
-
|
|
4360
|
-
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
|
|
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
|
+
});
|
|
4364
4777
|
});
|
|
4365
|
-
}
|
|
4778
|
+
}
|
|
4366
4779
|
(inner.proxies || []).forEach(function (px) {
|
|
4367
4780
|
eles.push({
|
|
4368
4781
|
data: {
|
|
@@ -4515,9 +4928,28 @@ var MODEL = __MODEL_JSON__;
|
|
|
4515
4928
|
|
|
4516
4929
|
var dimmedAnchors = {};
|
|
4517
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
|
+
|
|
4518
4947
|
var i = 0;
|
|
4519
4948
|
Object.keys(ve.agg).forEach(function (key) {
|
|
4520
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;
|
|
4521
4953
|
var bundle = e.n > 1;
|
|
4522
4954
|
var dim = state.query && (dimmedAnchors[e.src] || dimmedAnchors[e.tgt]);
|
|
4523
4955
|
var route = routeData(e.src, e.tgt, key);
|
|
@@ -4776,22 +5208,58 @@ var MODEL = __MODEL_JSON__;
|
|
|
4776
5208
|
}
|
|
4777
5209
|
return path;
|
|
4778
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
|
+
}
|
|
4779
5227
|
function renderCrumbs() {
|
|
4780
5228
|
var el = document.getElementById('crumbs');
|
|
4781
5229
|
var path = crumbPath();
|
|
4782
|
-
|
|
4783
|
-
|
|
4784
|
-
|
|
4785
|
-
+
|
|
4786
|
-
|
|
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;
|
|
4787
5252
|
var btns = el.querySelectorAll('button');
|
|
4788
5253
|
for (var i = 0; i < btns.length; i++) {
|
|
4789
5254
|
(function (b) {
|
|
5255
|
+
if (!b.getAttribute('data-ck')) return; // the "\\u2026" trigger toggles, never navigates
|
|
4790
5256
|
b.addEventListener('click', function () {
|
|
4791
5257
|
navigateTo(b.getAttribute('data-ck'), b.getAttribute('data-ci') || null);
|
|
4792
5258
|
});
|
|
4793
5259
|
})(btns[i]);
|
|
4794
5260
|
}
|
|
5261
|
+
if (crumbsCompact && path.length > 1) wireDropdown('crumbDd', 'crumbMoreBtn');
|
|
5262
|
+
if (headerReflowHook) headerReflowHook();
|
|
4795
5263
|
}
|
|
4796
5264
|
function renderViewHint() {
|
|
4797
5265
|
if (state.view.kind === 'types' || state.view.kind === 'databases') {
|
|
@@ -5058,7 +5526,16 @@ var MODEL = __MODEL_JSON__;
|
|
|
5058
5526
|
return c ? { kind: 'subsystem', id: c.subsystem } : { kind: 'system', id: null };
|
|
5059
5527
|
}
|
|
5060
5528
|
|
|
5061
|
-
|
|
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();
|
|
5062
5539
|
// Sync each View toggle's checkbox from the (possibly persisted) state, then
|
|
5063
5540
|
// persist on change so the choices survive a refresh (see persist()/saved).
|
|
5064
5541
|
document.getElementById('internalsToggle').checked = state.internals;
|
|
@@ -5112,9 +5589,24 @@ var MODEL = __MODEL_JSON__;
|
|
|
5112
5589
|
})(btns[i]);
|
|
5113
5590
|
}
|
|
5114
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
|
+
}
|
|
5115
5601
|
function updateHeaderSegs() {
|
|
5116
5602
|
var seg = document.getElementById('modeSeg');
|
|
5117
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
|
+
}
|
|
5118
5610
|
for (var i = 0; i < btns.length; i++) {
|
|
5119
5611
|
var vm = btns[i].getAttribute('data-vm');
|
|
5120
5612
|
var active = vm === 'components'
|
|
@@ -5122,6 +5614,7 @@ var MODEL = __MODEL_JSON__;
|
|
|
5122
5614
|
: vm === state.view.kind;
|
|
5123
5615
|
if (btns[i].classList) btns[i].classList[active ? 'add' : 'remove']('active');
|
|
5124
5616
|
}
|
|
5617
|
+
updateModeBtn();
|
|
5125
5618
|
var td = document.getElementById('typesDetailSeg');
|
|
5126
5619
|
td.style.display = (state.view.kind === 'types' || state.view.kind === 'databases') ? '' : 'none';
|
|
5127
5620
|
var tbs = td.querySelectorAll('button');
|
|
@@ -5185,7 +5678,13 @@ var MODEL = __MODEL_JSON__;
|
|
|
5185
5678
|
var r = btn.getBoundingClientRect();
|
|
5186
5679
|
menu.style.top = (r.bottom + 6) + 'px';
|
|
5187
5680
|
menu.style.left = 'auto';
|
|
5188
|
-
|
|
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';
|
|
5189
5688
|
}
|
|
5190
5689
|
function wireDropdown(ddId, btnId) {
|
|
5191
5690
|
var dd = document.getElementById(ddId);
|
|
@@ -5204,11 +5703,29 @@ var MODEL = __MODEL_JSON__;
|
|
|
5204
5703
|
var ldd = wireDropdown('layoutDd', 'layoutBtn');
|
|
5205
5704
|
var sdd = wireDropdown('settingsDd', 'settingsBtn');
|
|
5206
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
|
+
});
|
|
5207
5720
|
// Keep the settings panel open while flipping switches (clicks inside it don't
|
|
5208
5721
|
// bubble to the document-level close handler).
|
|
5209
5722
|
(function () {
|
|
5210
5723
|
var m = document.getElementById('settingsMenu');
|
|
5211
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(); });
|
|
5212
5729
|
})();
|
|
5213
5730
|
|
|
5214
5731
|
// Layout picker: choose the auto-layout algorithm. Components use cytoscape's
|
|
@@ -5232,6 +5749,12 @@ var MODEL = __MODEL_JSON__;
|
|
|
5232
5749
|
});
|
|
5233
5750
|
updateLayoutBtn();
|
|
5234
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
|
+
}
|
|
5235
5758
|
if (document.addEventListener) {
|
|
5236
5759
|
document.addEventListener('click', function () {
|
|
5237
5760
|
if (dd.classList) dd.classList.remove('open');
|
|
@@ -5239,6 +5762,9 @@ var MODEL = __MODEL_JSON__;
|
|
|
5239
5762
|
if (ldd.classList) ldd.classList.remove('open');
|
|
5240
5763
|
if (sdd.classList) sdd.classList.remove('open');
|
|
5241
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();
|
|
5242
5768
|
});
|
|
5243
5769
|
document.addEventListener('keydown', function (ev) {
|
|
5244
5770
|
if (ev.key === 'Escape') {
|
|
@@ -5246,16 +5772,27 @@ var MODEL = __MODEL_JSON__;
|
|
|
5246
5772
|
if (modal.classList && String(modal.className).indexOf('open') >= 0) { closeFlow(); return; }
|
|
5247
5773
|
setPresentation(false);
|
|
5248
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();
|
|
5249
5778
|
}
|
|
5250
5779
|
});
|
|
5251
5780
|
}
|
|
5252
5781
|
|
|
5253
|
-
// \u2500\u2500 Responsive header
|
|
5254
|
-
// When the floating header no longer fits its controls,
|
|
5255
|
-
//
|
|
5256
|
-
//
|
|
5257
|
-
//
|
|
5258
|
-
//
|
|
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.
|
|
5259
5796
|
(function () {
|
|
5260
5797
|
if (typeof window === 'undefined') return;
|
|
5261
5798
|
var hdr = document.getElementById('hdr');
|
|
@@ -5283,7 +5820,122 @@ var MODEL = __MODEL_JSON__;
|
|
|
5283
5820
|
}
|
|
5284
5821
|
return markers[id];
|
|
5285
5822
|
}
|
|
5286
|
-
|
|
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
|
+
];
|
|
5287
5939
|
// Signed fit measure in px: positive = overflowing, negative = headroom.
|
|
5288
5940
|
// The header is a flex row whose ONLY flex:1 child is the .spacer, so the
|
|
5289
5941
|
// spacer's rendered width IS the free space -- it grows to absorb all slack
|
|
@@ -5299,33 +5951,54 @@ var MODEL = __MODEL_JSON__;
|
|
|
5299
5951
|
var slack = spacer ? spacer.getBoundingClientRect().width : 0;
|
|
5300
5952
|
return (hdr.scrollWidth - hdr.clientWidth) - slack;
|
|
5301
5953
|
}
|
|
5954
|
+
var inReflow = false;
|
|
5302
5955
|
function reflow() {
|
|
5303
5956
|
// Not laid out (hidden tab, non-browser DOM) \u2014 measuring would misfire.
|
|
5304
5957
|
var box = hdr.getBoundingClientRect();
|
|
5305
5958
|
if (!box || box.width <= 0) return;
|
|
5306
|
-
|
|
5307
|
-
|
|
5308
|
-
|
|
5309
|
-
|
|
5310
|
-
|
|
5311
|
-
|
|
5312
|
-
|
|
5313
|
-
|
|
5314
|
-
|
|
5315
|
-
|
|
5316
|
-
|
|
5317
|
-
|
|
5318
|
-
|
|
5319
|
-
|
|
5320
|
-
|
|
5321
|
-
var
|
|
5322
|
-
|
|
5323
|
-
|
|
5324
|
-
|
|
5325
|
-
|
|
5326
|
-
|
|
5327
|
-
|
|
5328
|
-
|
|
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
|
+
}
|
|
5329
6002
|
}
|
|
5330
6003
|
var raf = null;
|
|
5331
6004
|
var defer = window.requestAnimationFrame
|
|
@@ -5335,6 +6008,9 @@ var MODEL = __MODEL_JSON__;
|
|
|
5335
6008
|
if (raf !== null) return;
|
|
5336
6009
|
raf = defer(function () { raf = null; reflow(); });
|
|
5337
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(); };
|
|
5338
6014
|
if (typeof ResizeObserver !== 'undefined') {
|
|
5339
6015
|
new ResizeObserver(schedule).observe(hdr);
|
|
5340
6016
|
} else if (window.addEventListener) {
|
|
@@ -6662,7 +7338,7 @@ var init_extensions = __esm({
|
|
|
6662
7338
|
});
|
|
6663
7339
|
|
|
6664
7340
|
// src/core/rules/types.ts
|
|
6665
|
-
var BUILTIN_PROFILES;
|
|
7341
|
+
var BUILTIN_PROFILES, PROJECT_KINDS;
|
|
6666
7342
|
var init_types = __esm({
|
|
6667
7343
|
"src/core/rules/types.ts"() {
|
|
6668
7344
|
"use strict";
|
|
@@ -6675,6 +7351,7 @@ var init_types = __esm({
|
|
|
6675
7351
|
"realtime-embedded",
|
|
6676
7352
|
"plc-cyclic"
|
|
6677
7353
|
];
|
|
7354
|
+
PROJECT_KINDS = ["fullstack", "system-of-systems", "monorepo"];
|
|
6678
7355
|
}
|
|
6679
7356
|
});
|
|
6680
7357
|
|
|
@@ -6855,44 +7532,13 @@ var init_type_references = __esm({
|
|
|
6855
7532
|
}
|
|
6856
7533
|
});
|
|
6857
7534
|
|
|
6858
|
-
// src/
|
|
6859
|
-
function
|
|
6860
|
-
|
|
6861
|
-
system: loadSystemSpec(),
|
|
6862
|
-
subsystems: loadSubsystemSpecs(),
|
|
6863
|
-
components: loadComponentSpecs(),
|
|
6864
|
-
interfaces: loadInterfaceSpecs(),
|
|
6865
|
-
implementations: loadImplementationSpecs(),
|
|
6866
|
-
types: loadTypeSpecs()
|
|
6867
|
-
};
|
|
6868
|
-
const digest = crypto.createHash("sha256").update(canonicalize(tree)).digest("hex");
|
|
6869
|
-
return { algorithm: "sha256", digest };
|
|
6870
|
-
}
|
|
6871
|
-
function stateIdEquals(a, b) {
|
|
6872
|
-
return !!a && !!b && a.algorithm === b.algorithm && a.digest === b.digest;
|
|
7535
|
+
// src/utils/filenames.ts
|
|
7536
|
+
function safeFilenamePart(value) {
|
|
7537
|
+
return value.replace(/[^a-zA-Z0-9._-]/g, "-");
|
|
6873
7538
|
}
|
|
6874
|
-
|
|
6875
|
-
|
|
6876
|
-
}
|
|
6877
|
-
function sortKeys(v) {
|
|
6878
|
-
if (Array.isArray(v)) return v.map(sortKeys);
|
|
6879
|
-
if (v && typeof v === "object") {
|
|
6880
|
-
const src = v;
|
|
6881
|
-
const out = {};
|
|
6882
|
-
for (const k of Object.keys(src).sort()) {
|
|
6883
|
-
if (k === "createdAt" || k === "updatedAt") continue;
|
|
6884
|
-
out[k] = sortKeys(src[k]);
|
|
6885
|
-
}
|
|
6886
|
-
return out;
|
|
6887
|
-
}
|
|
6888
|
-
return v;
|
|
6889
|
-
}
|
|
6890
|
-
var crypto;
|
|
6891
|
-
var init_statehash = __esm({
|
|
6892
|
-
"src/core/statehash.ts"() {
|
|
7539
|
+
var init_filenames = __esm({
|
|
7540
|
+
"src/utils/filenames.ts"() {
|
|
6893
7541
|
"use strict";
|
|
6894
|
-
crypto = __toESM(require("crypto"));
|
|
6895
|
-
init_specs2();
|
|
6896
7542
|
}
|
|
6897
7543
|
});
|
|
6898
7544
|
|
|
@@ -7349,6 +7995,66 @@ function projectOwnSurface(maxAudience) {
|
|
|
7349
7995
|
function projectChildSurface() {
|
|
7350
7996
|
return projectOwnSurface("project");
|
|
7351
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
|
+
}
|
|
7352
8058
|
function listSnapshots(rootDir = getProjectRoot()) {
|
|
7353
8059
|
const dir = surfacesDir(rootDir);
|
|
7354
8060
|
if (!fs9.existsSync(dir)) return [];
|
|
@@ -7365,39 +8071,66 @@ function listSnapshots(rootDir = getProjectRoot()) {
|
|
|
7365
8071
|
function getSnapshot(projectName, rootDir = getProjectRoot()) {
|
|
7366
8072
|
return listSnapshots(rootDir).find((s) => s.projectName === projectName) ?? null;
|
|
7367
8073
|
}
|
|
8074
|
+
function snapshotFilename(projectName) {
|
|
8075
|
+
return `${safeFilenamePart(projectName)}.yaml`;
|
|
8076
|
+
}
|
|
7368
8077
|
function saveSnapshot(snapshot, rootDir = getProjectRoot()) {
|
|
7369
8078
|
const dir = surfacesDir(rootDir);
|
|
7370
8079
|
fs9.mkdirSync(dir, { recursive: true });
|
|
7371
|
-
const p = path10.join(dir,
|
|
8080
|
+
const p = path10.join(dir, snapshotFilename(snapshot.projectName));
|
|
7372
8081
|
writeYamlFile(p, SurfaceSnapshotSchema.parse(snapshot));
|
|
7373
8082
|
return p;
|
|
7374
8083
|
}
|
|
7375
8084
|
function loadSurfaceSnapshots() {
|
|
7376
8085
|
return listSnapshots();
|
|
7377
8086
|
}
|
|
7378
|
-
function
|
|
7379
|
-
const
|
|
7380
|
-
|
|
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}`;
|
|
7381
8099
|
}
|
|
7382
|
-
function writeSurfaceFile(outPath,
|
|
8100
|
+
function writeSurfaceFile(outPath, snapshot, renderedSet) {
|
|
7383
8101
|
const resolved = path10.resolve(outPath);
|
|
7384
8102
|
fs9.mkdirSync(path10.dirname(resolved), { recursive: true });
|
|
7385
|
-
if (
|
|
7386
|
-
|
|
7387
|
-
|
|
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
|
+
});
|
|
7388
8116
|
}
|
|
7389
|
-
function
|
|
7390
|
-
const
|
|
7391
|
-
const openapi = format === "openapi" ? renderOpenApiForms(snapshot) : void 0;
|
|
7392
|
-
const body = openapi?.rendered ?? openapi?.renderedSet[0]?.document;
|
|
7393
|
-
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;
|
|
7394
8119
|
return {
|
|
7395
8120
|
snapshot,
|
|
7396
|
-
...
|
|
7397
|
-
...
|
|
7398
|
-
...
|
|
8121
|
+
...rendered !== void 0 ? { rendered } : {},
|
|
8122
|
+
...renderedSet ? { renderedSet } : {},
|
|
8123
|
+
...writtenPaths.length === 1 ? { writtenTo: writtenPaths[0] } : {},
|
|
8124
|
+
...writtenPaths.length ? { writtenPaths } : {}
|
|
7399
8125
|
};
|
|
7400
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
|
+
}
|
|
7401
8134
|
function importSurface(sourcePath, origin) {
|
|
7402
8135
|
const resolved = path10.resolve(sourcePath);
|
|
7403
8136
|
if (!fs9.existsSync(resolved)) {
|
|
@@ -7417,17 +8150,54 @@ function importSurface(sourcePath, origin) {
|
|
|
7417
8150
|
return snapshot;
|
|
7418
8151
|
}
|
|
7419
8152
|
function generateChildSnapshots(rootDir = getProjectRoot()) {
|
|
7420
|
-
const
|
|
8153
|
+
const topLevel = loadSubsystemSpecs().filter((s) => !s.id.includes("::"));
|
|
8154
|
+
const children = topLevel.filter((s) => s.projectPath);
|
|
7421
8155
|
if (!children.length) return [];
|
|
7422
|
-
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
|
+
};
|
|
7423
8166
|
const written = [];
|
|
7424
8167
|
for (const child of children) {
|
|
7425
8168
|
const childDir = path10.resolve(rootDir, child.projectPath);
|
|
7426
8169
|
if (!fs9.existsSync(childDir)) continue;
|
|
7427
|
-
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
|
+
}
|
|
7428
8175
|
}
|
|
7429
8176
|
return written;
|
|
7430
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
|
+
}
|
|
7431
8201
|
function surfaceContentKey(snapshot) {
|
|
7432
8202
|
const { stateId, generatedAt, origin, ...content } = snapshot;
|
|
7433
8203
|
return JSON.stringify(content);
|
|
@@ -7454,7 +8224,7 @@ function checkChildSurfaceFreshness(rootDir = getProjectRoot()) {
|
|
|
7454
8224
|
}
|
|
7455
8225
|
return issues;
|
|
7456
8226
|
}
|
|
7457
|
-
var fs9, path10, SURFACES_DIRNAME;
|
|
8227
|
+
var fs9, path10, SURFACES_DIRNAME, CROSS_BOUNDARY_TARGETS;
|
|
7458
8228
|
var init_surfaces = __esm({
|
|
7459
8229
|
"src/core/surfaces.ts"() {
|
|
7460
8230
|
"use strict";
|
|
@@ -7462,12 +8232,14 @@ var init_surfaces = __esm({
|
|
|
7462
8232
|
path10 = __toESM(require("path"));
|
|
7463
8233
|
init_fs();
|
|
7464
8234
|
init_yaml();
|
|
8235
|
+
init_filenames();
|
|
7465
8236
|
init_models();
|
|
7466
8237
|
init_specs2();
|
|
7467
8238
|
init_statehash();
|
|
7468
8239
|
init_type_analysis();
|
|
7469
8240
|
init_openapi();
|
|
7470
8241
|
SURFACES_DIRNAME = "surfaces";
|
|
8242
|
+
CROSS_BOUNDARY_TARGETS = /* @__PURE__ */ new Set(["Portal", "Gateway", "Observer"]);
|
|
7471
8243
|
}
|
|
7472
8244
|
});
|
|
7473
8245
|
|
|
@@ -7635,7 +8407,8 @@ var init_contracts = __esm({
|
|
|
7635
8407
|
"SURFACE_REF_NOT_EXPOSED",
|
|
7636
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}".`,
|
|
7637
8409
|
impl.id,
|
|
7638
|
-
isDraftCtx
|
|
8410
|
+
isDraftCtx,
|
|
8411
|
+
true
|
|
7639
8412
|
);
|
|
7640
8413
|
}
|
|
7641
8414
|
continue;
|
|
@@ -7692,7 +8465,8 @@ var init_contracts = __esm({
|
|
|
7692
8465
|
"SURFACE_REF_NOT_EXPOSED",
|
|
7693
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}".`,
|
|
7694
8467
|
impl.id,
|
|
7695
|
-
isDraftCtx
|
|
8468
|
+
isDraftCtx,
|
|
8469
|
+
true
|
|
7696
8470
|
);
|
|
7697
8471
|
} else if (step.assertsGuarantees) {
|
|
7698
8472
|
const declared = new Set(surfaceMethod.guarantees ?? []);
|
|
@@ -7703,7 +8477,8 @@ var init_contracts = __esm({
|
|
|
7703
8477
|
"NARRATIVE_SEMANTIC_UNBACKED",
|
|
7704
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}".`,
|
|
7705
8479
|
impl.id,
|
|
7706
|
-
isDraftCtx
|
|
8480
|
+
isDraftCtx,
|
|
8481
|
+
true
|
|
7707
8482
|
);
|
|
7708
8483
|
}
|
|
7709
8484
|
}
|
|
@@ -9054,13 +9829,14 @@ var init_portals = __esm({
|
|
|
9054
9829
|
};
|
|
9055
9830
|
portalsRule = {
|
|
9056
9831
|
name: "portal-endpoints",
|
|
9057
|
-
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).",
|
|
9058
9833
|
codes: [
|
|
9059
9834
|
{ code: "MISSING_PORTAL_TYPE", defaultSeverity: "error", summary: "Portal without a portalType" },
|
|
9060
9835
|
{ code: "MISSING_ENDPOINT", defaultSeverity: "error", summary: "Portal method without a wire endpoint binding" },
|
|
9061
9836
|
{ code: "ENDPOINT_TRANSPORT_MISMATCH", defaultSeverity: "error", summary: "Endpoint transport does not match the Portal portalType" },
|
|
9062
9837
|
{ code: "UNEXPECTED_PORTAL_FIELD", defaultSeverity: "error", summary: "Non-Portal component with portalType/basePath" },
|
|
9063
|
-
{ 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)" }
|
|
9064
9840
|
],
|
|
9065
9841
|
check(ctx) {
|
|
9066
9842
|
for (const comp of ctx.components) {
|
|
@@ -9112,6 +9888,15 @@ var init_portals = __esm({
|
|
|
9112
9888
|
isDraftCtx
|
|
9113
9889
|
);
|
|
9114
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
|
+
}
|
|
9115
9900
|
const compInterfaces = ctx.interfaces.filter((i) => i.component === comp.id);
|
|
9116
9901
|
for (const intf of compInterfaces) {
|
|
9117
9902
|
for (const m of intf.methods) {
|
|
@@ -9181,7 +9966,8 @@ var init_stereotype_deps = __esm({
|
|
|
9181
9966
|
"CROSS_SUBSYSTEM_NON_ADAPTER",
|
|
9182
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.`,
|
|
9183
9968
|
comp.id,
|
|
9184
|
-
isDraftCtx
|
|
9969
|
+
isDraftCtx,
|
|
9970
|
+
true
|
|
9185
9971
|
);
|
|
9186
9972
|
}
|
|
9187
9973
|
continue;
|
|
@@ -9759,14 +10545,14 @@ var init_declarative_assertions = __esm({
|
|
|
9759
10545
|
});
|
|
9760
10546
|
|
|
9761
10547
|
// src/core/rules/profiles.ts
|
|
9762
|
-
var BACKEND_LIKE, FRONTEND_LIKE,
|
|
10548
|
+
var BACKEND_LIKE, FRONTEND_LIKE, PROJECT_KINDS2, profilesRule;
|
|
9763
10549
|
var init_profiles = __esm({
|
|
9764
10550
|
"src/core/rules/profiles.ts"() {
|
|
9765
10551
|
"use strict";
|
|
9766
10552
|
init_types();
|
|
9767
10553
|
BACKEND_LIKE = /* @__PURE__ */ new Set(["backend", "lowlevel-os", "game-ecs", "realtime-embedded", "plc-cyclic"]);
|
|
9768
10554
|
FRONTEND_LIKE = /* @__PURE__ */ new Set(["frontend-reactive", "frontend-controller"]);
|
|
9769
|
-
|
|
10555
|
+
PROJECT_KINDS2 = new Set(PROJECT_KINDS);
|
|
9770
10556
|
profilesRule = {
|
|
9771
10557
|
name: "architectural-profiles",
|
|
9772
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.",
|
|
@@ -9790,7 +10576,7 @@ var init_profiles = __esm({
|
|
|
9790
10576
|
);
|
|
9791
10577
|
}
|
|
9792
10578
|
}
|
|
9793
|
-
if (!registered.has(ctx.projectType) && !
|
|
10579
|
+
if (!registered.has(ctx.projectType) && !PROJECT_KINDS2.has(ctx.projectType)) {
|
|
9794
10580
|
ctx.addIssue(
|
|
9795
10581
|
"warning",
|
|
9796
10582
|
"UNKNOWN_PROFILE",
|
|
@@ -11044,11 +11830,11 @@ var init_narrative_antipatterns = __esm({
|
|
|
11044
11830
|
const memberEdges = keys.flatMap((k) => (adjacency.get(k) ?? []).filter((e) => inScc.has(e.toKey)));
|
|
11045
11831
|
if (memberEdges.length === 0) continue;
|
|
11046
11832
|
const anchor = [...memberEdges].sort((a, b) => a.fromKey.localeCompare(b.fromKey))[0];
|
|
11047
|
-
const
|
|
11833
|
+
const path61 = [...keys].sort().join(" \u2192 ");
|
|
11048
11834
|
ctx.addIssue(
|
|
11049
11835
|
"warning",
|
|
11050
11836
|
"UNCONDITIONAL_CALL_CYCLE",
|
|
11051
|
-
`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.`,
|
|
11052
11838
|
anchor.impl.id,
|
|
11053
11839
|
memberEdges.some((e) => ctx.isImplementationDraft(e.impl))
|
|
11054
11840
|
);
|
|
@@ -12196,19 +12982,25 @@ var init_lint_allows = __esm({
|
|
|
12196
12982
|
});
|
|
12197
12983
|
|
|
12198
12984
|
// src/core/rules/portal-call-auth.ts
|
|
12199
|
-
var portalCallAuthRule;
|
|
12985
|
+
var COMPONENT_REF_PREFIX, portalCallAuthRule;
|
|
12200
12986
|
var init_portal_call_auth = __esm({
|
|
12201
12987
|
"src/core/rules/portal-call-auth.ts"() {
|
|
12202
12988
|
"use strict";
|
|
12989
|
+
COMPONENT_REF_PREFIX = "component:";
|
|
12203
12990
|
portalCallAuthRule = {
|
|
12204
12991
|
name: "portal-call-auth",
|
|
12205
|
-
description: "An OUTBOUND narrative `call` into ANOTHER component's Portal whose auth is not `none` must declare the credential source it presents
|
|
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.",
|
|
12206
12993
|
codes: [
|
|
12207
|
-
{ code: "PORTAL_AUTH_UNMET", defaultSeverity: "warning", summary: "A narrative call into another component's authed Portal does not declare where its credential loads from" }
|
|
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" }
|
|
12208
12999
|
],
|
|
12209
13000
|
check(ctx) {
|
|
12210
13001
|
for (const impl of ctx.implementations) {
|
|
12211
13002
|
const ownComponent = ctx.interfaceMap.get(impl.contract)?.component;
|
|
13003
|
+
const presenter = ownComponent ? ctx.componentMap.get(ownComponent) : void 0;
|
|
12212
13004
|
const draft = ctx.isImplementationDraft(impl);
|
|
12213
13005
|
for (const method2 of impl.methods ?? []) {
|
|
12214
13006
|
for (const step of method2.narrative ?? []) {
|
|
@@ -12217,14 +13009,59 @@ var init_portal_call_auth = __esm({
|
|
|
12217
13009
|
const target = ctx.componentMap.get(step.targetComponent);
|
|
12218
13010
|
if (!target || target.componentType !== "Portal") continue;
|
|
12219
13011
|
if (!target.auth || target.auth.scheme === "none") continue;
|
|
12220
|
-
if (
|
|
12221
|
-
|
|
12222
|
-
|
|
12223
|
-
|
|
12224
|
-
|
|
12225
|
-
|
|
12226
|
-
|
|
12227
|
-
|
|
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
|
+
}
|
|
12228
13065
|
}
|
|
12229
13066
|
}
|
|
12230
13067
|
}
|
|
@@ -12384,9 +13221,15 @@ function buildRuleContext(opts) {
|
|
|
12384
13221
|
...[...SDD_RULES, ...extensions.rules].flatMap((r) => r.codes.map((c) => c.code)),
|
|
12385
13222
|
// Declarative assertions bring their own namespaced codes — lint.allow
|
|
12386
13223
|
// and severity overrides treat them exactly like builtins.
|
|
12387
|
-
...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"
|
|
12388
13231
|
]);
|
|
12389
|
-
const addIssue = (defaultSeverity, code, message, specId, isDraftContext) => {
|
|
13232
|
+
const addIssue = (defaultSeverity, code, message, specId, isDraftContext, surfaceResolved) => {
|
|
12390
13233
|
if (scopeSubsystem && specId && !isSpecInScope(specId)) {
|
|
12391
13234
|
return;
|
|
12392
13235
|
}
|
|
@@ -12404,7 +13247,14 @@ function buildRuleContext(opts) {
|
|
|
12404
13247
|
if (severity === "warning") return;
|
|
12405
13248
|
}
|
|
12406
13249
|
}
|
|
12407
|
-
issues.push({
|
|
13250
|
+
issues.push({
|
|
13251
|
+
severity,
|
|
13252
|
+
code,
|
|
13253
|
+
message,
|
|
13254
|
+
specId,
|
|
13255
|
+
...isDraftContext ? { draftContext: true } : {},
|
|
13256
|
+
...surfaceResolved ? { surfaceResolved: true } : {}
|
|
13257
|
+
});
|
|
12408
13258
|
};
|
|
12409
13259
|
return {
|
|
12410
13260
|
system,
|
|
@@ -12829,24 +13679,54 @@ function validateSddTree(rulesOrOptions, projectType = "backend") {
|
|
|
12829
13679
|
for (const rule of ruleSequence()) {
|
|
12830
13680
|
rule.check(ctx);
|
|
12831
13681
|
}
|
|
12832
|
-
const hasCrossTreeSuspects = issues.some(
|
|
13682
|
+
const hasCrossTreeSuspects = issues.some(
|
|
13683
|
+
(i) => SUBPROJECT_REFERENCE_CODES.has(i.code) || SUBPROJECT_CONFORMANCE_CODES.has(i.code)
|
|
13684
|
+
);
|
|
12833
13685
|
const chainingParent = hasCrossTreeSuspects ? findChainingParent(getProjectRoot()) : null;
|
|
12834
13686
|
if (chainingParent) {
|
|
13687
|
+
let unverified = 0;
|
|
12835
13688
|
let downgraded = 0;
|
|
12836
|
-
for (
|
|
12837
|
-
|
|
12838
|
-
if (iss.
|
|
12839
|
-
|
|
12840
|
-
|
|
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;
|
|
12841
13711
|
}
|
|
12842
|
-
iss.crossTreeContext = true;
|
|
12843
13712
|
}
|
|
12844
|
-
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
|
+
}
|
|
12845
13725
|
issues.unshift({
|
|
12846
13726
|
severity: "warning",
|
|
12847
13727
|
code: "CHAINED_SUBPROJECT_CONTEXT",
|
|
12848
13728
|
crossTreeContext: true,
|
|
12849
|
-
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.`
|
|
12850
13730
|
});
|
|
12851
13731
|
}
|
|
12852
13732
|
}
|
|
@@ -12863,7 +13743,7 @@ function validateSddTree(rulesOrOptions, projectType = "backend") {
|
|
|
12863
13743
|
function validateAsComplete(options) {
|
|
12864
13744
|
return validateSddTree({ ...options ?? {}, treatAllAsComplete: true });
|
|
12865
13745
|
}
|
|
12866
|
-
var
|
|
13746
|
+
var SUBPROJECT_REFERENCE_CODES, SUBPROJECT_CONFORMANCE_CODES;
|
|
12867
13747
|
var init_validation = __esm({
|
|
12868
13748
|
"src/core/validation.ts"() {
|
|
12869
13749
|
"use strict";
|
|
@@ -12876,8 +13756,7 @@ var init_validation = __esm({
|
|
|
12876
13756
|
init_source_analysis();
|
|
12877
13757
|
init_specs2();
|
|
12878
13758
|
init_fs();
|
|
12879
|
-
|
|
12880
|
-
// reference resolution
|
|
13759
|
+
SUBPROJECT_REFERENCE_CODES = /* @__PURE__ */ new Set([
|
|
12881
13760
|
"UNDEFINED_TYPE_REFERENCE",
|
|
12882
13761
|
"INVALID_DEPENDENCY_REFERENCE",
|
|
12883
13762
|
"INVALID_TARGET_COMPONENT_REFERENCE",
|
|
@@ -12885,8 +13764,9 @@ var init_validation = __esm({
|
|
|
12885
13764
|
"UNDECLARED_DEPENDENCY_CALL",
|
|
12886
13765
|
"INVALID_TRUSTED_LINK",
|
|
12887
13766
|
"CROSS_SUBSYSTEM_NON_ADAPTER",
|
|
12888
|
-
"CROSS_TREE_REF_UNRESOLVED"
|
|
12889
|
-
|
|
13767
|
+
"CROSS_TREE_REF_UNRESOLVED"
|
|
13768
|
+
]);
|
|
13769
|
+
SUBPROJECT_CONFORMANCE_CODES = /* @__PURE__ */ new Set([
|
|
12890
13770
|
"MISSING_SOURCE_FILE",
|
|
12891
13771
|
"SOURCE_PATH_ESCAPES_ROOT",
|
|
12892
13772
|
"MISSING_SOURCE_PATH",
|
|
@@ -13428,6 +14308,7 @@ __export(specs_exports, {
|
|
|
13428
14308
|
buildProjectGraph: () => buildProjectGraph,
|
|
13429
14309
|
clearLoaderIssues: () => clearLoaderIssues,
|
|
13430
14310
|
collectPromotableSpecs: () => collectPromotableSpecs,
|
|
14311
|
+
computeStateIdAt: () => computeStateIdAt,
|
|
13431
14312
|
deleteComponentSpec: () => deleteComponentSpec,
|
|
13432
14313
|
deleteGroupSpec: () => deleteGroupSpec,
|
|
13433
14314
|
deleteImplementationSpec: () => deleteImplementationSpec,
|
|
@@ -13460,6 +14341,7 @@ __export(specs_exports, {
|
|
|
13460
14341
|
loadTypeSpec: () => loadTypeSpec,
|
|
13461
14342
|
loadTypeSpecs: () => loadTypeSpecs,
|
|
13462
14343
|
normalizeComponentLayout: () => normalizeComponentLayout,
|
|
14344
|
+
resolveChainingParent: () => resolveChainingParent,
|
|
13463
14345
|
resolveSubprojectForNamespace: () => resolveSubprojectForNamespace,
|
|
13464
14346
|
restoreSpecFiles: () => restoreSpecFiles,
|
|
13465
14347
|
saveComponentSpec: () => saveComponentSpec,
|
|
@@ -13864,6 +14746,19 @@ function dryRunSerializeSpecs(include) {
|
|
|
13864
14746
|
function buildProjectGraph(level) {
|
|
13865
14747
|
return buildGraphModel(level);
|
|
13866
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
|
+
}
|
|
13867
14762
|
function deleteTypeSpec(id) {
|
|
13868
14763
|
return current().deleteTypeSpec(id);
|
|
13869
14764
|
}
|
|
@@ -13907,6 +14802,7 @@ var init_specs2 = __esm({
|
|
|
13907
14802
|
path14 = __toESM(require("path"));
|
|
13908
14803
|
init_loader();
|
|
13909
14804
|
init_fs();
|
|
14805
|
+
init_statehash();
|
|
13910
14806
|
init_yaml();
|
|
13911
14807
|
init_models();
|
|
13912
14808
|
init_narrative_labels();
|
|
@@ -16500,6 +17396,9 @@ function requireSpecs() {
|
|
|
16500
17396
|
function requireProvision() {
|
|
16501
17397
|
return init_provision(), __toCommonJS(provision_exports);
|
|
16502
17398
|
}
|
|
17399
|
+
function listExternalInterfaces2() {
|
|
17400
|
+
return listExternalInterfaces();
|
|
17401
|
+
}
|
|
16503
17402
|
function text(content) {
|
|
16504
17403
|
return { content: [{ type: "text", text: content }] };
|
|
16505
17404
|
}
|
|
@@ -17129,7 +18028,7 @@ NOTICE:
|
|
|
17129
18028
|
type: import_zod9.z.enum(["local", "call", "dispatch", "branch", "switch", "loop", "try", "parallel", "jump", "return", "throw"]),
|
|
17130
18029
|
targetComponent: import_zod9.z.string().optional().describe("call/dispatch: L2 component id (for dispatch, the Portal routed through)"),
|
|
17131
18030
|
targetMethod: import_zod9.z.string().optional().describe("call: method name on the target"),
|
|
17132
|
-
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`
|
|
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)."),
|
|
17133
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)"),
|
|
17134
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)"),
|
|
17135
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)"),
|
|
@@ -17466,7 +18365,36 @@ NOTICE:
|
|
|
17466
18365
|
}
|
|
17467
18366
|
}
|
|
17468
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
|
+
);
|
|
17469
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
|
+
}
|
|
17470
18398
|
if (options.hostedTools) {
|
|
17471
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.");
|
|
17472
18400
|
reg(server, "sdd_host_lock_project", {
|
|
@@ -17602,6 +18530,8 @@ var init_server = __esm({
|
|
|
17602
18530
|
init_narrative_labels();
|
|
17603
18531
|
init_specs();
|
|
17604
18532
|
init_skills();
|
|
18533
|
+
init_specs2();
|
|
18534
|
+
init_surfaces();
|
|
17605
18535
|
SERVER_BUILD_STAMP = captureBuildStamp(__filename);
|
|
17606
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.";
|
|
17607
18537
|
SKILL_RESOURCE_MIME = "text/markdown";
|
|
@@ -20675,7 +21605,7 @@ var require_dist = __commonJS({
|
|
|
20675
21605
|
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
20676
21606
|
}
|
|
20677
21607
|
var fs51 = __toESM2(require("fs"));
|
|
20678
|
-
var
|
|
21608
|
+
var path61 = __toESM2(require("path"));
|
|
20679
21609
|
var import_fflate = require_node();
|
|
20680
21610
|
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", ".hg", ".svn"]);
|
|
20681
21611
|
function listEntries(archive) {
|
|
@@ -20713,8 +21643,8 @@ var require_dist = __commonJS({
|
|
|
20713
21643
|
}
|
|
20714
21644
|
function writeTree(destDir, files) {
|
|
20715
21645
|
for (const file of files) {
|
|
20716
|
-
const absolute =
|
|
20717
|
-
fs51.mkdirSync(
|
|
21646
|
+
const absolute = path61.join(destDir, file.path);
|
|
21647
|
+
fs51.mkdirSync(path61.dirname(absolute), { recursive: true });
|
|
20718
21648
|
fs51.writeFileSync(absolute, file.contents);
|
|
20719
21649
|
}
|
|
20720
21650
|
}
|
|
@@ -20727,10 +21657,10 @@ var require_dist = __commonJS({
|
|
|
20727
21657
|
for (const entry of fs51.readdirSync(current2, { withFileTypes: true })) {
|
|
20728
21658
|
if (entry.isDirectory()) {
|
|
20729
21659
|
if (SKIP_DIRS.has(entry.name)) continue;
|
|
20730
|
-
walkPackDir(root,
|
|
21660
|
+
walkPackDir(root, path61.join(current2, entry.name), out);
|
|
20731
21661
|
} else if (entry.isFile()) {
|
|
20732
|
-
const absolute =
|
|
20733
|
-
const relative22 =
|
|
21662
|
+
const absolute = path61.join(current2, entry.name);
|
|
21663
|
+
const relative22 = path61.relative(root, absolute).split(path61.sep).join("/");
|
|
20734
21664
|
out.push({ path: relative22, contents: fs51.readFileSync(absolute) });
|
|
20735
21665
|
}
|
|
20736
21666
|
}
|
|
@@ -22141,86 +23071,216 @@ async function generateLayer(options = {}) {
|
|
|
22141
23071
|
}
|
|
22142
23072
|
|
|
22143
23073
|
// src/commands/lock.ts
|
|
23074
|
+
var os7 = __toESM(require("os"));
|
|
22144
23075
|
var import_inquirer2 = __toESM(require("inquirer"));
|
|
22145
23076
|
init_logger();
|
|
22146
|
-
|
|
22147
|
-
|
|
22148
|
-
|
|
22149
|
-
|
|
22150
|
-
|
|
22151
|
-
|
|
22152
|
-
|
|
22153
|
-
|
|
22154
|
-
|
|
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) });
|
|
22155
23097
|
}
|
|
22156
|
-
const
|
|
22157
|
-
|
|
22158
|
-
|
|
22159
|
-
const promotable = collectPromotableSpecs(options.subsystem);
|
|
22160
|
-
const originalStatuses = /* @__PURE__ */ new Map();
|
|
22161
|
-
const isSpecInSubsystemScope = (specSubsystem) => {
|
|
22162
|
-
if (!options.subsystem) return true;
|
|
22163
|
-
if (!specSubsystem) return false;
|
|
22164
|
-
return specSubsystem === options.subsystem || specSubsystem.startsWith(`${options.subsystem}::`);
|
|
22165
|
-
};
|
|
22166
|
-
for (const s of index.subsystems) {
|
|
22167
|
-
if (!options.subsystem || s.id === options.subsystem || s.id.startsWith(`${options.subsystem}::`)) {
|
|
22168
|
-
originalStatuses.set(s, s.status);
|
|
22169
|
-
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) });
|
|
22170
23101
|
}
|
|
22171
23102
|
}
|
|
22172
|
-
|
|
22173
|
-
|
|
22174
|
-
|
|
22175
|
-
|
|
22176
|
-
|
|
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) });
|
|
22177
23113
|
}
|
|
22178
|
-
|
|
22179
|
-
|
|
22180
|
-
|
|
22181
|
-
|
|
22182
|
-
|
|
22183
|
-
|
|
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);
|
|
22184
23121
|
}
|
|
22185
|
-
for (const
|
|
22186
|
-
|
|
22187
|
-
const comp = intf ? index.components.find((c) => c.id === intf.component) : null;
|
|
22188
|
-
if (comp && isSpecInSubsystemScope(comp.subsystem)) {
|
|
22189
|
-
originalStatuses.set(m, m.status);
|
|
22190
|
-
m.status = "complete";
|
|
22191
|
-
}
|
|
23122
|
+
for (const c of candidates) {
|
|
23123
|
+
idCount.set(c.suggestedId, (idCount.get(c.suggestedId) ?? 0) + 1);
|
|
22192
23124
|
}
|
|
22193
|
-
|
|
22194
|
-
|
|
22195
|
-
|
|
22196
|
-
|
|
22197
|
-
|
|
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 };
|
|
22198
23130
|
});
|
|
22199
|
-
|
|
22200
|
-
|
|
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
|
+
}
|
|
22201
23150
|
}
|
|
22202
|
-
|
|
22203
|
-
|
|
22204
|
-
|
|
22205
|
-
|
|
22206
|
-
|
|
22207
|
-
|
|
22208
|
-
|
|
22209
|
-
|
|
22210
|
-
|
|
22211
|
-
|
|
22212
|
-
|
|
22213
|
-
|
|
22214
|
-
|
|
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;
|
|
22215
23194
|
}
|
|
22216
|
-
|
|
22217
|
-
|
|
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
|
+
});
|
|
22218
23226
|
}
|
|
22219
|
-
|
|
22220
|
-
logger.info("Fix the errors above, then run `wairon lock` again. Nothing was changed.");
|
|
22221
|
-
process.exit(1);
|
|
23227
|
+
walkForPackages(projectRoot2, fullPath, depth + 1, results);
|
|
22222
23228
|
}
|
|
22223
|
-
|
|
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);
|
|
22224
23284
|
if (promotable.length === 0) {
|
|
22225
23285
|
logger.info("All specs are already complete \u2014 this will re-validate and regenerate the agent topology.");
|
|
22226
23286
|
} else {
|
|
@@ -22244,23 +23304,36 @@ async function runLock(options = {}) {
|
|
|
22244
23304
|
default: false
|
|
22245
23305
|
}
|
|
22246
23306
|
]);
|
|
22247
|
-
if (!confirmed)
|
|
22248
|
-
|
|
22249
|
-
|
|
22250
|
-
|
|
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();
|
|
22251
23314
|
}
|
|
22252
|
-
for (const p of promotable) applySpecStatus(p.kind, p.id, "complete");
|
|
22253
|
-
invalidateSpecCache();
|
|
22254
23315
|
if (promotable.length > 0) {
|
|
22255
23316
|
logger.success(`Locked ${promotable.length} spec(s) as complete.`);
|
|
22256
23317
|
}
|
|
22257
|
-
|
|
22258
|
-
|
|
22259
|
-
|
|
22260
|
-
|
|
22261
|
-
|
|
22262
|
-
|
|
22263
|
-
|
|
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;
|
|
22264
23337
|
}
|
|
22265
23338
|
|
|
22266
23339
|
// src/commands/validate.ts
|
|
@@ -22399,6 +23472,10 @@ async function runValidate(options = {}) {
|
|
|
22399
23472
|
}
|
|
22400
23473
|
}
|
|
22401
23474
|
|
|
23475
|
+
// src/cli/index.ts
|
|
23476
|
+
init_loader();
|
|
23477
|
+
init_fs();
|
|
23478
|
+
|
|
22402
23479
|
// src/commands/list.ts
|
|
22403
23480
|
var import_chalk7 = __toESM(require("chalk"));
|
|
22404
23481
|
init_logger();
|
|
@@ -22513,9 +23590,9 @@ init_mcp();
|
|
|
22513
23590
|
// src/commands/update.ts
|
|
22514
23591
|
var https = __toESM(require("https"));
|
|
22515
23592
|
var http = __toESM(require("http"));
|
|
22516
|
-
var
|
|
22517
|
-
var
|
|
22518
|
-
var
|
|
23593
|
+
var fs20 = __toESM(require("fs"));
|
|
23594
|
+
var path29 = __toESM(require("path"));
|
|
23595
|
+
var os8 = __toESM(require("os"));
|
|
22519
23596
|
var crypto2 = __toESM(require("crypto"));
|
|
22520
23597
|
var import_child_process2 = require("child_process");
|
|
22521
23598
|
init_logger();
|
|
@@ -22578,8 +23655,8 @@ async function runUpdate(options = {}) {
|
|
|
22578
23655
|
logger.info(`Download manually from: ${release.html_url}`);
|
|
22579
23656
|
process.exit(1);
|
|
22580
23657
|
}
|
|
22581
|
-
const tmpDir =
|
|
22582
|
-
const tmpFile =
|
|
23658
|
+
const tmpDir = os8.tmpdir();
|
|
23659
|
+
const tmpFile = path29.join(tmpDir, assetName);
|
|
22583
23660
|
logger.info(`Downloading ${assetName}...`);
|
|
22584
23661
|
try {
|
|
22585
23662
|
await downloadFile(asset.browser_download_url, tmpFile);
|
|
@@ -22596,16 +23673,16 @@ async function runUpdate(options = {}) {
|
|
|
22596
23673
|
const checksumAssetName = assetName + ".sha256";
|
|
22597
23674
|
const checksumAsset = release.assets.find((a) => a.name === checksumAssetName);
|
|
22598
23675
|
if (checksumAsset) {
|
|
22599
|
-
const tmpChecksum =
|
|
23676
|
+
const tmpChecksum = path29.join(tmpDir, checksumAssetName);
|
|
22600
23677
|
logger.info(`Verifying checksum...`);
|
|
22601
23678
|
try {
|
|
22602
23679
|
await downloadFile(checksumAsset.browser_download_url, tmpChecksum);
|
|
22603
23680
|
verifyChecksum(tmpFile, tmpChecksum, assetName);
|
|
22604
|
-
|
|
23681
|
+
fs20.unlinkSync(tmpChecksum);
|
|
22605
23682
|
} catch (err) {
|
|
22606
23683
|
logger.error(`Checksum verification failed: ${err.message}`);
|
|
22607
23684
|
try {
|
|
22608
|
-
|
|
23685
|
+
fs20.unlinkSync(tmpFile);
|
|
22609
23686
|
} catch {
|
|
22610
23687
|
}
|
|
22611
23688
|
process.exit(1);
|
|
@@ -22671,7 +23748,7 @@ function fetchReleases(repo) {
|
|
|
22671
23748
|
}
|
|
22672
23749
|
function downloadFile(url, dest) {
|
|
22673
23750
|
return new Promise((resolve24, reject) => {
|
|
22674
|
-
const file =
|
|
23751
|
+
const file = fs20.createWriteStream(dest);
|
|
22675
23752
|
const get3 = url.startsWith("https://") ? https.get : http.get;
|
|
22676
23753
|
get3(url, { headers: { "User-Agent": `wairon/${WAIRON_VERSION}` }, agent: false }, (res) => {
|
|
22677
23754
|
if (res.statusCode === 301 || res.statusCode === 302) {
|
|
@@ -22693,21 +23770,21 @@ function downloadFile(url, dest) {
|
|
|
22693
23770
|
});
|
|
22694
23771
|
file.on("error", (err) => {
|
|
22695
23772
|
res.destroy();
|
|
22696
|
-
|
|
23773
|
+
fs20.unlink(dest, () => {
|
|
22697
23774
|
});
|
|
22698
23775
|
reject(err);
|
|
22699
23776
|
});
|
|
22700
23777
|
}).on("error", (err) => {
|
|
22701
|
-
|
|
23778
|
+
fs20.unlink(dest, () => {
|
|
22702
23779
|
});
|
|
22703
23780
|
reject(err);
|
|
22704
23781
|
});
|
|
22705
23782
|
});
|
|
22706
23783
|
}
|
|
22707
23784
|
function verifyChecksum(filePath, checksumFile, expectedFilename) {
|
|
22708
|
-
const checksumContent =
|
|
23785
|
+
const checksumContent = fs20.readFileSync(checksumFile, "utf-8").trim();
|
|
22709
23786
|
const expectedHash = checksumContent.split(/\s+/)[0].toLowerCase();
|
|
22710
|
-
const fileBuffer =
|
|
23787
|
+
const fileBuffer = fs20.readFileSync(filePath);
|
|
22711
23788
|
const actualHash = crypto2.createHash("sha256").update(fileBuffer).digest("hex").toLowerCase();
|
|
22712
23789
|
if (actualHash !== expectedHash) {
|
|
22713
23790
|
throw new Error(
|
|
@@ -22738,9 +23815,9 @@ function isPkgBinary2() {
|
|
|
22738
23815
|
function installBinary(tmpFile, destPath) {
|
|
22739
23816
|
const platform = process.platform;
|
|
22740
23817
|
const isZip = tmpFile.endsWith(".zip");
|
|
22741
|
-
const extractDir =
|
|
22742
|
-
if (
|
|
22743
|
-
|
|
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 });
|
|
22744
23821
|
if (isZip) {
|
|
22745
23822
|
(0, import_child_process2.execSync)(
|
|
22746
23823
|
`powershell -NoProfile -NonInteractive -Command "Expand-Archive -Path '${tmpFile}' -DestinationPath '${extractDir}' -Force"`,
|
|
@@ -22750,18 +23827,18 @@ function installBinary(tmpFile, destPath) {
|
|
|
22750
23827
|
(0, import_child_process2.execSync)(`tar -xzf "${tmpFile}" -C "${extractDir}"`, { stdio: ["ignore", "pipe", "pipe"] });
|
|
22751
23828
|
}
|
|
22752
23829
|
const binaryName = platform === "win32" ? "wairon.exe" : "wairon";
|
|
22753
|
-
const extractedBinary =
|
|
22754
|
-
if (!
|
|
23830
|
+
const extractedBinary = path29.join(extractDir, binaryName);
|
|
23831
|
+
if (!fs20.existsSync(extractedBinary)) {
|
|
22755
23832
|
throw new Error(`Extracted binary not found at ${extractedBinary}`);
|
|
22756
23833
|
}
|
|
22757
23834
|
if (platform === "win32") {
|
|
22758
23835
|
const oldPath = destPath + ".old";
|
|
22759
23836
|
try {
|
|
22760
23837
|
cleanStaleBinary(oldPath);
|
|
22761
|
-
|
|
22762
|
-
|
|
23838
|
+
fs20.renameSync(destPath, oldPath);
|
|
23839
|
+
fs20.copyFileSync(extractedBinary, destPath);
|
|
22763
23840
|
try {
|
|
22764
|
-
|
|
23841
|
+
fs20.unlinkSync(oldPath);
|
|
22765
23842
|
} catch {
|
|
22766
23843
|
}
|
|
22767
23844
|
} catch (err) {
|
|
@@ -22775,25 +23852,25 @@ function installBinary(tmpFile, destPath) {
|
|
|
22775
23852
|
}
|
|
22776
23853
|
} else {
|
|
22777
23854
|
const tmpDest = destPath + ".new";
|
|
22778
|
-
|
|
22779
|
-
|
|
22780
|
-
|
|
23855
|
+
fs20.copyFileSync(extractedBinary, tmpDest);
|
|
23856
|
+
fs20.chmodSync(tmpDest, 493);
|
|
23857
|
+
fs20.renameSync(tmpDest, destPath);
|
|
22781
23858
|
}
|
|
22782
23859
|
try {
|
|
22783
|
-
|
|
23860
|
+
fs20.unlinkSync(tmpFile);
|
|
22784
23861
|
} catch {
|
|
22785
23862
|
}
|
|
22786
23863
|
try {
|
|
22787
|
-
|
|
23864
|
+
fs20.rmSync(extractDir, { recursive: true });
|
|
22788
23865
|
} catch {
|
|
22789
23866
|
}
|
|
22790
23867
|
}
|
|
22791
23868
|
function cleanStaleBinary(oldPath) {
|
|
22792
23869
|
const target = oldPath ?? (isPkgBinary2() ? process.execPath + ".old" : null);
|
|
22793
23870
|
if (!target) return;
|
|
22794
|
-
if (
|
|
23871
|
+
if (fs20.existsSync(target)) {
|
|
22795
23872
|
try {
|
|
22796
|
-
|
|
23873
|
+
fs20.unlinkSync(target);
|
|
22797
23874
|
} catch {
|
|
22798
23875
|
}
|
|
22799
23876
|
}
|
|
@@ -23039,171 +24116,6 @@ async function filteredCheckbox(config) {
|
|
|
23039
24116
|
|
|
23040
24117
|
// src/commands/domains.ts
|
|
23041
24118
|
init_loader();
|
|
23042
|
-
|
|
23043
|
-
// src/core/detection.ts
|
|
23044
|
-
var fs19 = __toESM(require("fs"));
|
|
23045
|
-
var path28 = __toESM(require("path"));
|
|
23046
|
-
init_defaults();
|
|
23047
|
-
var PACKAGE_MARKERS = [
|
|
23048
|
-
"package.json",
|
|
23049
|
-
"pyproject.toml",
|
|
23050
|
-
"Cargo.toml",
|
|
23051
|
-
"go.mod",
|
|
23052
|
-
"build.gradle",
|
|
23053
|
-
"build.gradle.kts",
|
|
23054
|
-
"pom.xml"
|
|
23055
|
-
];
|
|
23056
|
-
var MAX_SCAN_DEPTH = 5;
|
|
23057
|
-
function detectDomainCandidates(projectRoot2, alreadyTrackedPaths = /* @__PURE__ */ new Set(), alreadyTrackedIds = /* @__PURE__ */ new Set()) {
|
|
23058
|
-
const candidates = /* @__PURE__ */ new Map();
|
|
23059
|
-
for (const c of detectGitSubmodules(projectRoot2)) {
|
|
23060
|
-
candidates.set(c.path, { ...c, alreadyTracked: alreadyTrackedPaths.has(c.path) });
|
|
23061
|
-
}
|
|
23062
|
-
for (const c of detectNestedGitRepos(projectRoot2)) {
|
|
23063
|
-
if (!candidates.has(c.path)) {
|
|
23064
|
-
candidates.set(c.path, { ...c, alreadyTracked: alreadyTrackedPaths.has(c.path) });
|
|
23065
|
-
}
|
|
23066
|
-
}
|
|
23067
|
-
const gitPaths = new Set(
|
|
23068
|
-
Array.from(candidates.values()).filter((c) => c.type === "git-submodule" || c.type === "git-repo").map((c) => c.path)
|
|
23069
|
-
);
|
|
23070
|
-
for (const c of detectPackageRoots(projectRoot2)) {
|
|
23071
|
-
if (candidates.has(c.path)) continue;
|
|
23072
|
-
const insideGit = Array.from(gitPaths).some(
|
|
23073
|
-
(gp) => c.path === gp || c.path.startsWith(gp + "/")
|
|
23074
|
-
);
|
|
23075
|
-
if (insideGit) continue;
|
|
23076
|
-
candidates.set(c.path, { ...c, alreadyTracked: alreadyTrackedPaths.has(c.path) });
|
|
23077
|
-
}
|
|
23078
|
-
const sorted = Array.from(candidates.values()).sort((a, b) => a.path.localeCompare(b.path));
|
|
23079
|
-
return deduplicateIds(sorted, alreadyTrackedIds);
|
|
23080
|
-
}
|
|
23081
|
-
function deduplicateIds(candidates, existingIds = /* @__PURE__ */ new Set()) {
|
|
23082
|
-
const idCount = /* @__PURE__ */ new Map();
|
|
23083
|
-
for (const id of existingIds) {
|
|
23084
|
-
idCount.set(id, (idCount.get(id) ?? 0) + 1);
|
|
23085
|
-
}
|
|
23086
|
-
for (const c of candidates) {
|
|
23087
|
-
idCount.set(c.suggestedId, (idCount.get(c.suggestedId) ?? 0) + 1);
|
|
23088
|
-
}
|
|
23089
|
-
return candidates.map((c) => {
|
|
23090
|
-
if ((idCount.get(c.suggestedId) ?? 0) <= 1) return c;
|
|
23091
|
-
const parts = c.path.split("/");
|
|
23092
|
-
const qualifiedId2 = parts.length >= 2 ? pathToId(`${parts[parts.length - 2]}-${parts[parts.length - 1]}`) : c.suggestedId;
|
|
23093
|
-
return { ...c, suggestedId: qualifiedId2 };
|
|
23094
|
-
});
|
|
23095
|
-
}
|
|
23096
|
-
function parseGitmodules(filePath) {
|
|
23097
|
-
const content = fs19.readFileSync(filePath, "utf-8");
|
|
23098
|
-
const entries = [];
|
|
23099
|
-
let current2 = {};
|
|
23100
|
-
for (const line2 of content.split("\n")) {
|
|
23101
|
-
const trimmed = line2.trim();
|
|
23102
|
-
const headerMatch = trimmed.match(/^\[submodule "(.+)"\]$/);
|
|
23103
|
-
if (headerMatch) {
|
|
23104
|
-
if (current2.path) entries.push(current2);
|
|
23105
|
-
current2 = { name: headerMatch[1] };
|
|
23106
|
-
continue;
|
|
23107
|
-
}
|
|
23108
|
-
const keyVal = trimmed.match(/^(\w+)\s*=\s*(.+)$/);
|
|
23109
|
-
if (keyVal) {
|
|
23110
|
-
const [, key, value] = keyVal;
|
|
23111
|
-
if (key === "path") current2.path = value.trim();
|
|
23112
|
-
if (key === "url") current2.url = value.trim();
|
|
23113
|
-
}
|
|
23114
|
-
}
|
|
23115
|
-
if (current2.path) entries.push(current2);
|
|
23116
|
-
return entries;
|
|
23117
|
-
}
|
|
23118
|
-
function detectGitSubmodules(projectRoot2) {
|
|
23119
|
-
const gitmodulesPath = path28.join(projectRoot2, ".gitmodules");
|
|
23120
|
-
if (!fs19.existsSync(gitmodulesPath)) return [];
|
|
23121
|
-
return parseGitmodules(gitmodulesPath).map((entry) => ({
|
|
23122
|
-
suggestedId: pathToId(entry.path),
|
|
23123
|
-
suggestedName: pathToName(entry.path),
|
|
23124
|
-
path: normalizePath3(entry.path),
|
|
23125
|
-
type: "git-submodule",
|
|
23126
|
-
alreadyTracked: false
|
|
23127
|
-
}));
|
|
23128
|
-
}
|
|
23129
|
-
function detectNestedGitRepos(projectRoot2) {
|
|
23130
|
-
const results = [];
|
|
23131
|
-
walkForGit(projectRoot2, projectRoot2, 0, results);
|
|
23132
|
-
return results;
|
|
23133
|
-
}
|
|
23134
|
-
function walkForGit(projectRoot2, currentDir, depth, results) {
|
|
23135
|
-
if (depth > MAX_SCAN_DEPTH) return;
|
|
23136
|
-
let entries;
|
|
23137
|
-
try {
|
|
23138
|
-
entries = fs19.readdirSync(currentDir, { withFileTypes: true });
|
|
23139
|
-
} catch {
|
|
23140
|
-
return;
|
|
23141
|
-
}
|
|
23142
|
-
for (const entry of entries) {
|
|
23143
|
-
if (!entry.isDirectory()) continue;
|
|
23144
|
-
if (SCAN_EXCLUDE_DIRS.has(entry.name)) continue;
|
|
23145
|
-
const fullPath = path28.join(currentDir, entry.name);
|
|
23146
|
-
const relPath = normalizePath3(path28.relative(projectRoot2, fullPath));
|
|
23147
|
-
if (relPath === "" || relPath === ".") continue;
|
|
23148
|
-
const gitPath = path28.join(fullPath, ".git");
|
|
23149
|
-
if (fs19.existsSync(gitPath)) {
|
|
23150
|
-
results.push({
|
|
23151
|
-
suggestedId: pathToId(relPath),
|
|
23152
|
-
suggestedName: pathToName(relPath),
|
|
23153
|
-
path: relPath,
|
|
23154
|
-
type: "git-repo",
|
|
23155
|
-
alreadyTracked: false
|
|
23156
|
-
});
|
|
23157
|
-
continue;
|
|
23158
|
-
}
|
|
23159
|
-
walkForGit(projectRoot2, fullPath, depth + 1, results);
|
|
23160
|
-
}
|
|
23161
|
-
}
|
|
23162
|
-
function detectPackageRoots(projectRoot2) {
|
|
23163
|
-
const results = [];
|
|
23164
|
-
walkForPackages(projectRoot2, projectRoot2, 0, results);
|
|
23165
|
-
return results;
|
|
23166
|
-
}
|
|
23167
|
-
function walkForPackages(projectRoot2, currentDir, depth, results) {
|
|
23168
|
-
if (depth > MAX_SCAN_DEPTH) return;
|
|
23169
|
-
let entries;
|
|
23170
|
-
try {
|
|
23171
|
-
entries = fs19.readdirSync(currentDir, { withFileTypes: true });
|
|
23172
|
-
} catch {
|
|
23173
|
-
return;
|
|
23174
|
-
}
|
|
23175
|
-
for (const entry of entries) {
|
|
23176
|
-
if (!entry.isDirectory()) continue;
|
|
23177
|
-
if (SCAN_EXCLUDE_DIRS.has(entry.name)) continue;
|
|
23178
|
-
const fullPath = path28.join(currentDir, entry.name);
|
|
23179
|
-
const relPath = normalizePath3(path28.relative(projectRoot2, fullPath));
|
|
23180
|
-
if (relPath === "" || relPath === ".") continue;
|
|
23181
|
-
const hasMarker = PACKAGE_MARKERS.some((m) => fs19.existsSync(path28.join(fullPath, m)));
|
|
23182
|
-
if (hasMarker) {
|
|
23183
|
-
results.push({
|
|
23184
|
-
suggestedId: pathToId(relPath),
|
|
23185
|
-
suggestedName: pathToName(relPath),
|
|
23186
|
-
path: relPath,
|
|
23187
|
-
type: "package-root",
|
|
23188
|
-
alreadyTracked: false
|
|
23189
|
-
});
|
|
23190
|
-
}
|
|
23191
|
-
walkForPackages(projectRoot2, fullPath, depth + 1, results);
|
|
23192
|
-
}
|
|
23193
|
-
}
|
|
23194
|
-
function pathToId(relPath) {
|
|
23195
|
-
const basename12 = path28.basename(relPath);
|
|
23196
|
-
return basename12.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
23197
|
-
}
|
|
23198
|
-
function pathToName(relPath) {
|
|
23199
|
-
const id = pathToId(relPath);
|
|
23200
|
-
return id.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
23201
|
-
}
|
|
23202
|
-
function normalizePath3(p) {
|
|
23203
|
-
return p.replace(/\\/g, "/");
|
|
23204
|
-
}
|
|
23205
|
-
|
|
23206
|
-
// src/commands/domains.ts
|
|
23207
24119
|
init_domains();
|
|
23208
24120
|
init_domain();
|
|
23209
24121
|
async function runDomainsList() {
|
|
@@ -23393,9 +24305,9 @@ async function runSkillsInstall() {
|
|
|
23393
24305
|
}
|
|
23394
24306
|
|
|
23395
24307
|
// src/commands/doctor.ts
|
|
23396
|
-
var
|
|
23397
|
-
var
|
|
23398
|
-
var
|
|
24308
|
+
var fs21 = __toESM(require("fs"));
|
|
24309
|
+
var os9 = __toESM(require("os"));
|
|
24310
|
+
var path30 = __toESM(require("path"));
|
|
23399
24311
|
var import_chalk12 = __toESM(require("chalk"));
|
|
23400
24312
|
init_logger();
|
|
23401
24313
|
init_defaults();
|
|
@@ -23425,10 +24337,10 @@ function stampVerdict(content) {
|
|
|
23425
24337
|
return { mark: "warn", note: `v${v} \u2014 stale, installed is v${WAIRON_VERSION}` };
|
|
23426
24338
|
}
|
|
23427
24339
|
function mcpEntryHealth(settingsPath) {
|
|
23428
|
-
if (!
|
|
24340
|
+
if (!fs21.existsSync(settingsPath)) return { mark: "warn", note: "not registered" };
|
|
23429
24341
|
let entry;
|
|
23430
24342
|
try {
|
|
23431
|
-
const s = JSON.parse(
|
|
24343
|
+
const s = JSON.parse(fs21.readFileSync(settingsPath, "utf8"));
|
|
23432
24344
|
entry = s.mcpServers?.["wairon"];
|
|
23433
24345
|
} catch {
|
|
23434
24346
|
return { mark: "error", note: "parse error" };
|
|
@@ -23436,7 +24348,7 @@ function mcpEntryHealth(settingsPath) {
|
|
|
23436
24348
|
if (!entry) return { mark: "warn", note: "not registered" };
|
|
23437
24349
|
if (entry.command === "node" && Array.isArray(entry.args) && typeof entry.args[0] === "string") {
|
|
23438
24350
|
const scriptPath = entry.args[0];
|
|
23439
|
-
if (!
|
|
24351
|
+
if (!fs21.existsSync(scriptPath)) {
|
|
23440
24352
|
return { mark: "error", note: `registered but the server path is missing \u2014 ${scriptPath}` };
|
|
23441
24353
|
}
|
|
23442
24354
|
}
|
|
@@ -23488,7 +24400,7 @@ async function runDoctor(options = {}) {
|
|
|
23488
24400
|
const { findChainingSubprojectsMissingConfig: findChainingSubprojectsMissingConfig2 } = (init_provision(), __toCommonJS(provision_exports));
|
|
23489
24401
|
const missing = findChainingSubprojectsMissingConfig2(getProjectRoot());
|
|
23490
24402
|
if (missing.length > 0) {
|
|
23491
|
-
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.`);
|
|
23492
24404
|
}
|
|
23493
24405
|
} catch {
|
|
23494
24406
|
}
|
|
@@ -23526,7 +24438,7 @@ async function runDoctor(options = {}) {
|
|
|
23526
24438
|
const gp = localGuideFilePath(process.cwd(), t);
|
|
23527
24439
|
if (!gp || seenGuides.has(gp)) continue;
|
|
23528
24440
|
seenGuides.add(gp);
|
|
23529
|
-
const rel2 =
|
|
24441
|
+
const rel2 = path30.relative(process.cwd(), gp).replace(/\\/g, "/");
|
|
23530
24442
|
if (!pathExists(gp)) {
|
|
23531
24443
|
line(tally, "warn", `${rel2} guide \u2014 not injected (run \`wairon generate\`)`);
|
|
23532
24444
|
continue;
|
|
@@ -23560,17 +24472,17 @@ async function runDoctor(options = {}) {
|
|
|
23560
24472
|
line(tally, h.mark, `Claude (project .mcp.json): ${h.note}${h.mark === "ok" ? "" : " \u2014 run `wairon mcp install --backend claude`"}`);
|
|
23561
24473
|
}
|
|
23562
24474
|
if (wantGemini) {
|
|
23563
|
-
const globalCfg =
|
|
24475
|
+
const globalCfg = path30.join(os9.homedir(), ".gemini", "antigravity-cli", "mcp_config.json");
|
|
23564
24476
|
const hg = mcpEntryHealth(globalCfg);
|
|
23565
24477
|
line(tally, hg.mark, `Antigravity (global mcp_config.json): ${hg.note}${hg.mark === "ok" ? "" : " \u2014 run `wairon mcp install --backend gemini --global`"}`);
|
|
23566
24478
|
const projPath = fromProjectRoot(".gemini", "settings.json");
|
|
23567
|
-
if (
|
|
24479
|
+
if (fs21.existsSync(projPath)) {
|
|
23568
24480
|
const hp = mcpEntryHealth(projPath);
|
|
23569
24481
|
line(tally, hp.mark === "error" ? "error" : "ok", `Gemini CLI (project): ${hp.note} ${import_chalk12.default.gray("(Antigravity ignores this file)")}`);
|
|
23570
24482
|
}
|
|
23571
24483
|
}
|
|
23572
|
-
const pluginDir =
|
|
23573
|
-
if (
|
|
24484
|
+
const pluginDir = path30.join(os9.homedir(), ".gemini", "config", "plugins", "wairon");
|
|
24485
|
+
if (fs21.existsSync(pluginDir)) {
|
|
23574
24486
|
line(tally, "warn", `Legacy Antigravity plugin present (${pluginDir}) \u2014 it collides with the wairon MCP server. Remove it with \`wairon doctor --fix\`.`);
|
|
23575
24487
|
}
|
|
23576
24488
|
logger.blank();
|
|
@@ -23601,7 +24513,7 @@ async function applyFixes() {
|
|
|
23601
24513
|
const legacySpecs = findLegacySpecFiles();
|
|
23602
24514
|
if (legacySpecs.length > 0) {
|
|
23603
24515
|
for (const { path: oldPath, expected: newPath } of legacySpecs) {
|
|
23604
|
-
|
|
24516
|
+
fs21.renameSync(oldPath, newPath);
|
|
23605
24517
|
}
|
|
23606
24518
|
console.log(` ${icon("ok")} Migrated ${legacySpecs.length} legacy spec file(s) to the new dot-prefixed unified schema.`);
|
|
23607
24519
|
}
|
|
@@ -23655,8 +24567,8 @@ function printSummary(tally) {
|
|
|
23655
24567
|
}
|
|
23656
24568
|
|
|
23657
24569
|
// src/commands/diagram.ts
|
|
23658
|
-
var
|
|
23659
|
-
var
|
|
24570
|
+
var fs22 = __toESM(require("fs"));
|
|
24571
|
+
var path31 = __toESM(require("path"));
|
|
23660
24572
|
init_logger();
|
|
23661
24573
|
init_loader();
|
|
23662
24574
|
init_fs();
|
|
@@ -23691,8 +24603,8 @@ function collectIssues() {
|
|
|
23691
24603
|
}
|
|
23692
24604
|
function writeCanvas(dest) {
|
|
23693
24605
|
const model = buildCanvasModel(collectIssues());
|
|
23694
|
-
ensureDir(
|
|
23695
|
-
|
|
24606
|
+
ensureDir(path31.dirname(path31.resolve(dest)));
|
|
24607
|
+
fs22.writeFileSync(dest, renderCanvasHtml(model), "utf-8");
|
|
23696
24608
|
}
|
|
23697
24609
|
function parseSequenceRef(ref) {
|
|
23698
24610
|
const sep6 = ref.includes(":") ? ref.lastIndexOf(":") : ref.lastIndexOf(".");
|
|
@@ -23707,55 +24619,55 @@ async function runDiagram(rawOptions = {}) {
|
|
|
23707
24619
|
assertProjectInitialized();
|
|
23708
24620
|
const options = applyFormat(rawOptions);
|
|
23709
24621
|
if (options.canvas && !options.all) {
|
|
23710
|
-
const dest2 = options.out ??
|
|
24622
|
+
const dest2 = options.out ?? path31.join(AI_PATHS.docsDir(), "diagrams", "canvas.html");
|
|
23711
24623
|
writeCanvas(dest2);
|
|
23712
24624
|
logger.success(`Interactive canvas written to ${dest2}`);
|
|
23713
24625
|
logger.info("Open it in a browser \u2014 fully self-contained (works offline).");
|
|
23714
24626
|
return;
|
|
23715
24627
|
}
|
|
23716
24628
|
if (options.drawio && !options.all) {
|
|
23717
|
-
const dest2 = options.out ??
|
|
23718
|
-
ensureDir(
|
|
23719
|
-
|
|
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");
|
|
23720
24632
|
logger.success(`draw.io diagram written to ${dest2}`);
|
|
23721
24633
|
logger.info("Open with draw.io / diagrams.net (or import into tools that accept the format).");
|
|
23722
24634
|
return;
|
|
23723
24635
|
}
|
|
23724
24636
|
if (options.excalidraw && !options.all) {
|
|
23725
|
-
const dest2 = options.out ??
|
|
23726
|
-
ensureDir(
|
|
23727
|
-
|
|
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");
|
|
23728
24640
|
logger.success(`Excalidraw scene written to ${dest2}`);
|
|
23729
24641
|
logger.info("Open with excalidraw.com or the VS Code extension.");
|
|
23730
24642
|
return;
|
|
23731
24643
|
}
|
|
23732
24644
|
const wantsMermaid = options.format?.toLowerCase().startsWith("mermaid") || !!options.subsystem || !!options.sequence;
|
|
23733
24645
|
if (!options.all && !options.sequence && !wantsMermaid) {
|
|
23734
|
-
const dest2 = options.out ??
|
|
24646
|
+
const dest2 = options.out ?? path31.join(AI_PATHS.docsDir(), "diagrams", "canvas.html");
|
|
23735
24647
|
writeCanvas(dest2);
|
|
23736
24648
|
logger.success(`Interactive canvas written to ${dest2}`);
|
|
23737
24649
|
logger.info("Open it in a browser \u2014 fully self-contained (works offline). Other formats: --format mermaid|drawio|excalidraw.");
|
|
23738
24650
|
return;
|
|
23739
24651
|
}
|
|
23740
24652
|
if (options.all) {
|
|
23741
|
-
const outDir = options.out ??
|
|
24653
|
+
const outDir = options.out ?? path31.join(AI_PATHS.docsDir(), "diagrams");
|
|
23742
24654
|
const files = generateDiagramSet();
|
|
23743
24655
|
if (files.length === 0) {
|
|
23744
24656
|
logger.warn("No diagrams to generate \u2014 the spec tree has no components yet.");
|
|
23745
24657
|
return;
|
|
23746
24658
|
}
|
|
23747
24659
|
for (const file of files) {
|
|
23748
|
-
const dest2 =
|
|
23749
|
-
ensureDir(
|
|
23750
|
-
|
|
24660
|
+
const dest2 = path31.join(outDir, file.relPath);
|
|
24661
|
+
ensureDir(path31.dirname(dest2));
|
|
24662
|
+
fs22.writeFileSync(dest2, toMarkdown(file), "utf-8");
|
|
23751
24663
|
}
|
|
23752
|
-
writeCanvas(
|
|
24664
|
+
writeCanvas(path31.join(outDir, "canvas.html"));
|
|
23753
24665
|
const exportModel = buildCanvasModel();
|
|
23754
|
-
|
|
23755
|
-
|
|
24666
|
+
fs22.writeFileSync(path31.join(outDir, "architecture.drawio"), generateDrawioXml(exportModel), "utf-8");
|
|
24667
|
+
fs22.writeFileSync(path31.join(outDir, "architecture.excalidraw"), generateExcalidrawScene(exportModel), "utf-8");
|
|
23756
24668
|
const graph = loadSpecGraph();
|
|
23757
|
-
const indexPath =
|
|
23758
|
-
|
|
24669
|
+
const indexPath = path31.join(outDir, "README.md");
|
|
24670
|
+
fs22.writeFileSync(indexPath, diagramSetIndex(files, graph.systemName), "utf-8");
|
|
23759
24671
|
logger.success(`Generated ${files.length} diagram(s) + interactive canvas.html + index into ${outDir}`);
|
|
23760
24672
|
for (const file of files.slice(0, 12)) {
|
|
23761
24673
|
logger.info(` ${file.relPath}`);
|
|
@@ -23766,26 +24678,26 @@ async function runDiagram(rawOptions = {}) {
|
|
|
23766
24678
|
let mermaid;
|
|
23767
24679
|
let title;
|
|
23768
24680
|
let defaultDest;
|
|
23769
|
-
const diagramsDir =
|
|
24681
|
+
const diagramsDir = path31.join(AI_PATHS.docsDir(), "diagrams");
|
|
23770
24682
|
if (options.sequence) {
|
|
23771
24683
|
const { component, method: method2 } = parseSequenceRef(options.sequence);
|
|
23772
24684
|
mermaid = generateSequenceDiagram(component, method2, { depth: options.depth });
|
|
23773
24685
|
title = `${component}.${method2} \u2014 narrative sequence`;
|
|
23774
|
-
defaultDest =
|
|
24686
|
+
defaultDest = path31.join(diagramsDir, "sequences", `${component.replace(/::/g, "--")}.${method2}.md`);
|
|
23775
24687
|
} else if (options.subsystem) {
|
|
23776
24688
|
mermaid = generateComponentDiagram({ subsystem: options.subsystem });
|
|
23777
24689
|
title = `${options.subsystem} \u2014 components`;
|
|
23778
|
-
defaultDest =
|
|
24690
|
+
defaultDest = path31.join(diagramsDir, "subsystems", `${options.subsystem.replace(/::/g, "--")}.md`);
|
|
23779
24691
|
} else {
|
|
23780
24692
|
mermaid = generateComponentDiagram();
|
|
23781
24693
|
title = "Component architecture";
|
|
23782
|
-
defaultDest =
|
|
24694
|
+
defaultDest = path31.join(diagramsDir, "system.md");
|
|
23783
24695
|
}
|
|
23784
24696
|
const dest = options.out ?? defaultDest;
|
|
23785
|
-
ensureDir(
|
|
24697
|
+
ensureDir(path31.dirname(path31.resolve(dest)));
|
|
23786
24698
|
const content = dest.endsWith(".mmd") ? `${mermaid}
|
|
23787
24699
|
` : toMarkdown({ relPath: dest, title, mermaid });
|
|
23788
|
-
|
|
24700
|
+
fs22.writeFileSync(dest, content, "utf-8");
|
|
23789
24701
|
logger.success(`Mermaid diagram written to ${dest}`);
|
|
23790
24702
|
logger.info("Renders on GitHub/IDE previews; use a .mmd --out path for raw Mermaid.");
|
|
23791
24703
|
}
|
|
@@ -23894,8 +24806,8 @@ Component variants (${variants.length})
|
|
|
23894
24806
|
}
|
|
23895
24807
|
|
|
23896
24808
|
// src/commands/packs.ts
|
|
23897
|
-
var
|
|
23898
|
-
var
|
|
24809
|
+
var fs23 = __toESM(require("fs"));
|
|
24810
|
+
var path32 = __toESM(require("path"));
|
|
23899
24811
|
var import_chalk16 = __toESM(require("chalk"));
|
|
23900
24812
|
var import_sdk = __toESM(require_dist());
|
|
23901
24813
|
init_logger();
|
|
@@ -23921,11 +24833,11 @@ function describe(probe2) {
|
|
|
23921
24833
|
return parts.join(", ");
|
|
23922
24834
|
}
|
|
23923
24835
|
function resolveSourceUnit(source) {
|
|
23924
|
-
const abs =
|
|
23925
|
-
if (!
|
|
24836
|
+
const abs = path32.resolve(source);
|
|
24837
|
+
if (!fs23.existsSync(abs)) {
|
|
23926
24838
|
throw new Error(`Pack source "${source}" does not exist.`);
|
|
23927
24839
|
}
|
|
23928
|
-
const isDir =
|
|
24840
|
+
const isDir = fs23.statSync(abs).isDirectory();
|
|
23929
24841
|
if (isDir && !packDirEntry(abs)) {
|
|
23930
24842
|
throw new Error(`"${source}" is a directory without a pack entry file (pack.yaml | pack.cjs | index.cjs | ...).`);
|
|
23931
24843
|
}
|
|
@@ -23941,7 +24853,7 @@ async function addPack(source, options = {}) {
|
|
|
23941
24853
|
}
|
|
23942
24854
|
const { abs } = resolveSourceUnit(source);
|
|
23943
24855
|
const scope = options.global ? "global" : "project";
|
|
23944
|
-
const probe2 = probePack(abs,
|
|
24856
|
+
const probe2 = probePack(abs, path32.dirname(abs), scope);
|
|
23945
24857
|
if (probe2.error) {
|
|
23946
24858
|
logger.error(probe2.error);
|
|
23947
24859
|
process.exitCode = 1;
|
|
@@ -23949,10 +24861,10 @@ async function addPack(source, options = {}) {
|
|
|
23949
24861
|
}
|
|
23950
24862
|
if (options.global) {
|
|
23951
24863
|
const destDir = globalPacksDir();
|
|
23952
|
-
const dest2 =
|
|
23953
|
-
if (
|
|
23954
|
-
|
|
23955
|
-
|
|
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 });
|
|
23956
24868
|
}
|
|
23957
24869
|
logger.success(`Installed pack "${probe2.name}" globally: ${dest2}`);
|
|
23958
24870
|
logger.info(`${describe(probe2)} \u2014 auto-loaded for every project on this machine (WAIRON_PACKS_DIR / ~/.wairon/packs).`);
|
|
@@ -23965,11 +24877,11 @@ async function addPack(source, options = {}) {
|
|
|
23965
24877
|
return;
|
|
23966
24878
|
}
|
|
23967
24879
|
const root = getProjectRoot();
|
|
23968
|
-
const relRef = `.wai/packs/${
|
|
23969
|
-
const dest =
|
|
23970
|
-
if (
|
|
23971
|
-
|
|
23972
|
-
|
|
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 });
|
|
23973
24885
|
}
|
|
23974
24886
|
const config = loadProjectConfig();
|
|
23975
24887
|
const packs = config.extensions?.packs ?? [];
|
|
@@ -23978,14 +24890,14 @@ async function addPack(source, options = {}) {
|
|
|
23978
24890
|
saveProjectConfig(config);
|
|
23979
24891
|
logger.success(`Vendored pack "${probe2.name}" into ${relRef} and registered it in .wai/project.yaml.`);
|
|
23980
24892
|
} else {
|
|
23981
|
-
|
|
24893
|
+
fs23.cpSync(abs, dest, { recursive: true, force: true });
|
|
23982
24894
|
logger.success(`Pack "${probe2.name}" already registered \u2014 refreshed ${relRef} from the source.`);
|
|
23983
24895
|
}
|
|
23984
24896
|
logger.info(`${describe(probe2)} \u2014 commit .wai/ so CI and every clone enforce it.`);
|
|
23985
24897
|
}
|
|
23986
24898
|
async function addPackFromArchive(source, options) {
|
|
23987
|
-
const abs =
|
|
23988
|
-
if (!
|
|
24899
|
+
const abs = path32.resolve(source);
|
|
24900
|
+
if (!fs23.existsSync(abs) || !fs23.statSync(abs).isFile()) {
|
|
23989
24901
|
logger.error(`Pack archive "${source}" does not exist.`);
|
|
23990
24902
|
process.exitCode = 1;
|
|
23991
24903
|
return;
|
|
@@ -24000,27 +24912,27 @@ async function addPackFromArchive(source, options) {
|
|
|
24000
24912
|
process.exitCode = 1;
|
|
24001
24913
|
return;
|
|
24002
24914
|
}
|
|
24003
|
-
baseDir =
|
|
24915
|
+
baseDir = path32.join(getProjectRoot(), ".wai", "packs");
|
|
24004
24916
|
}
|
|
24005
|
-
const bytes =
|
|
24006
|
-
|
|
24007
|
-
const staging =
|
|
24917
|
+
const bytes = fs23.readFileSync(abs);
|
|
24918
|
+
fs23.mkdirSync(baseDir, { recursive: true });
|
|
24919
|
+
const staging = fs23.mkdtempSync(path32.join(baseDir, ".wpack-staging-"));
|
|
24008
24920
|
let result;
|
|
24009
24921
|
try {
|
|
24010
24922
|
result = (0, import_sdk.extractPack)(bytes, staging);
|
|
24011
24923
|
} catch (err) {
|
|
24012
|
-
|
|
24013
|
-
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)}`);
|
|
24014
24926
|
process.exitCode = 1;
|
|
24015
24927
|
return;
|
|
24016
24928
|
}
|
|
24017
24929
|
const name = result.name;
|
|
24018
|
-
const destDir =
|
|
24019
|
-
if (
|
|
24020
|
-
|
|
24021
|
-
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);
|
|
24022
24934
|
if (probe2.error) {
|
|
24023
|
-
|
|
24935
|
+
fs23.rmSync(destDir, { recursive: true, force: true });
|
|
24024
24936
|
logger.error(probe2.error);
|
|
24025
24937
|
process.exitCode = 1;
|
|
24026
24938
|
return;
|
|
@@ -24038,7 +24950,7 @@ async function addPackFromArchive(source, options) {
|
|
|
24038
24950
|
saveProjectConfig(config);
|
|
24039
24951
|
logger.success(`Installed pack "${probe2.name ?? name}" into ${relRef} and registered it in .wai/project.yaml.`);
|
|
24040
24952
|
} else {
|
|
24041
|
-
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)}.`);
|
|
24042
24954
|
}
|
|
24043
24955
|
logger.info(`${describe(probe2)} \u2014 commit .wai/ so CI and every clone enforce it.`);
|
|
24044
24956
|
}
|
|
@@ -24059,7 +24971,7 @@ async function buildPack(source, options = {}) {
|
|
|
24059
24971
|
const sourceDir = source && source.length > 0 ? source : ".";
|
|
24060
24972
|
const result = (0, import_sdk.buildPack)(sourceDir);
|
|
24061
24973
|
const outPath = options.out ?? result.suggestedFileName;
|
|
24062
|
-
|
|
24974
|
+
fs23.writeFileSync(outPath, result.archive);
|
|
24063
24975
|
logger.success(`Built pack "${result.info.name}" v${result.info.version} \u2192 ${outPath} (${result.archive.byteLength} bytes)`);
|
|
24064
24976
|
logger.info(`Install it with \`wairon pack add ${outPath}\`, or upload it to a hosted instance.`);
|
|
24065
24977
|
}
|
|
@@ -24073,9 +24985,9 @@ async function listPacks() {
|
|
|
24073
24985
|
console.log(import_chalk16.default.bold.cyan(`\u25A0 Global (${globalPacksDir()})${useGlobal ? "" : import_chalk16.default.yellow(" [disabled: extensions.useGlobalPacks: false]")}`));
|
|
24074
24986
|
if (globalRefs.length === 0) console.log(import_chalk16.default.dim(" (none)"));
|
|
24075
24987
|
for (const ref of globalRefs) {
|
|
24076
|
-
const probe2 = probePack(ref,
|
|
24077
|
-
if (probe2.error) console.log(` ${import_chalk16.default.red("\u2716")} ${
|
|
24078
|
-
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))}`);
|
|
24079
24991
|
}
|
|
24080
24992
|
console.log("");
|
|
24081
24993
|
if (!inProject) {
|
|
@@ -24096,9 +25008,9 @@ async function listPacks() {
|
|
|
24096
25008
|
async function removePack(name, options = {}) {
|
|
24097
25009
|
if (options.global) {
|
|
24098
25010
|
for (const ref of discoverPacks(globalPacksDir())) {
|
|
24099
|
-
const probe2 = probePack(ref,
|
|
24100
|
-
if (probe2.name === name ||
|
|
24101
|
-
|
|
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 });
|
|
24102
25014
|
logger.success(`Removed global pack "${probe2.name ?? name}" (${ref}).`);
|
|
24103
25015
|
return;
|
|
24104
25016
|
}
|
|
@@ -24117,16 +25029,16 @@ async function removePack(name, options = {}) {
|
|
|
24117
25029
|
const packs = config.extensions?.packs ?? [];
|
|
24118
25030
|
for (const ref of packs) {
|
|
24119
25031
|
const probe2 = probePack(ref, root, "project");
|
|
24120
|
-
if (probe2.name === name || ref === name ||
|
|
25032
|
+
if (probe2.name === name || ref === name || path32.basename(ref) === name) {
|
|
24121
25033
|
config.extensions = {
|
|
24122
25034
|
packs: packs.filter((p) => p !== ref),
|
|
24123
25035
|
useGlobalPacks: config.extensions?.useGlobalPacks ?? true
|
|
24124
25036
|
};
|
|
24125
25037
|
saveProjectConfig(config);
|
|
24126
|
-
const resolved =
|
|
24127
|
-
const vendorDir =
|
|
24128
|
-
if (resolved.startsWith(vendorDir +
|
|
24129
|
-
|
|
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 });
|
|
24130
25042
|
logger.success(`Deregistered pack "${probe2.name ?? name}" and deleted ${ref}.`);
|
|
24131
25043
|
} else {
|
|
24132
25044
|
logger.success(`Deregistered pack "${probe2.name ?? name}" (files at ${ref} left in place).`);
|
|
@@ -24140,8 +25052,8 @@ async function removePack(name, options = {}) {
|
|
|
24140
25052
|
|
|
24141
25053
|
// src/commands/host.ts
|
|
24142
25054
|
var fs50 = __toESM(require("fs"));
|
|
24143
|
-
var
|
|
24144
|
-
var
|
|
25055
|
+
var path59 = __toESM(require("path"));
|
|
25056
|
+
var os10 = __toESM(require("os"));
|
|
24145
25057
|
var crypto20 = __toESM(require("crypto"));
|
|
24146
25058
|
var import_child_process5 = require("child_process");
|
|
24147
25059
|
var import_chalk17 = __toESM(require("chalk"));
|
|
@@ -24168,29 +25080,29 @@ var UNAUTHENTICATED = {
|
|
|
24168
25080
|
var WEB_SESSION_PREFIX = "ws_";
|
|
24169
25081
|
|
|
24170
25082
|
// src/server/credentials.ts
|
|
24171
|
-
var
|
|
24172
|
-
var
|
|
25083
|
+
var fs24 = __toESM(require("fs"));
|
|
25084
|
+
var path33 = __toESM(require("path"));
|
|
24173
25085
|
var crypto3 = __toESM(require("crypto"));
|
|
24174
25086
|
var HASH_NS = "wairon:token:v1";
|
|
24175
25087
|
function hashToken(token) {
|
|
24176
25088
|
return crypto3.createHash("sha256").update(`${HASH_NS}:${token}`).digest("hex");
|
|
24177
25089
|
}
|
|
24178
25090
|
function storePath(dataDir) {
|
|
24179
|
-
return
|
|
25091
|
+
return path33.join(dataDir, "auth", "credentials.json");
|
|
24180
25092
|
}
|
|
24181
25093
|
function load3(dataDir) {
|
|
24182
25094
|
try {
|
|
24183
|
-
return JSON.parse(
|
|
25095
|
+
return JSON.parse(fs24.readFileSync(storePath(dataDir), "utf8"));
|
|
24184
25096
|
} catch {
|
|
24185
25097
|
return [];
|
|
24186
25098
|
}
|
|
24187
25099
|
}
|
|
24188
25100
|
function save(dataDir, records) {
|
|
24189
25101
|
const p = storePath(dataDir);
|
|
24190
|
-
|
|
25102
|
+
fs24.mkdirSync(path33.dirname(p), { recursive: true });
|
|
24191
25103
|
const tmp = `${p}.tmp`;
|
|
24192
|
-
|
|
24193
|
-
|
|
25104
|
+
fs24.writeFileSync(tmp, JSON.stringify(records, null, 2) + "\n");
|
|
25105
|
+
fs24.renameSync(tmp, p);
|
|
24194
25106
|
}
|
|
24195
25107
|
function digestEquals(a, b) {
|
|
24196
25108
|
const ab = Buffer.from(a, "hex");
|
|
@@ -24235,17 +25147,17 @@ function listByOwner(dataDir, ownerUserId) {
|
|
|
24235
25147
|
}
|
|
24236
25148
|
|
|
24237
25149
|
// src/server/websessions.ts
|
|
24238
|
-
var
|
|
24239
|
-
var
|
|
25150
|
+
var fs25 = __toESM(require("fs"));
|
|
25151
|
+
var path34 = __toESM(require("path"));
|
|
24240
25152
|
var crypto4 = __toESM(require("crypto"));
|
|
24241
25153
|
function storePath2(dataDir) {
|
|
24242
|
-
return
|
|
25154
|
+
return path34.join(dataDir, "web-sessions.json");
|
|
24243
25155
|
}
|
|
24244
25156
|
function readSessions(dataDir) {
|
|
24245
25157
|
const p = storePath2(dataDir);
|
|
24246
25158
|
let raw;
|
|
24247
25159
|
try {
|
|
24248
|
-
raw =
|
|
25160
|
+
raw = fs25.readFileSync(p, "utf8");
|
|
24249
25161
|
} catch (e) {
|
|
24250
25162
|
if (e.code === "ENOENT") return [];
|
|
24251
25163
|
throw new Error(`Failed to read web session store at ${p}: ${e.message}`);
|
|
@@ -24260,10 +25172,10 @@ function readSessions(dataDir) {
|
|
|
24260
25172
|
}
|
|
24261
25173
|
function persistSessions(dataDir, sessions) {
|
|
24262
25174
|
const p = storePath2(dataDir);
|
|
24263
|
-
|
|
25175
|
+
fs25.mkdirSync(path34.dirname(p), { recursive: true });
|
|
24264
25176
|
const tmp = `${p}.tmp`;
|
|
24265
|
-
|
|
24266
|
-
|
|
25177
|
+
fs25.writeFileSync(tmp, JSON.stringify(sessions, null, 2) + "\n");
|
|
25178
|
+
fs25.renameSync(tmp, p);
|
|
24267
25179
|
}
|
|
24268
25180
|
function mintSessionId() {
|
|
24269
25181
|
return `${WEB_SESSION_PREFIX}${crypto4.randomBytes(24).toString("hex")}`;
|
|
@@ -24424,17 +25336,17 @@ function listWebSessionsBySubject(dataDir, userId) {
|
|
|
24424
25336
|
}
|
|
24425
25337
|
|
|
24426
25338
|
// src/server/users.ts
|
|
24427
|
-
var
|
|
24428
|
-
var
|
|
25339
|
+
var fs26 = __toESM(require("fs"));
|
|
25340
|
+
var path35 = __toESM(require("path"));
|
|
24429
25341
|
var VALID_STATUSES = ["active", "inactive", "suspended", "deactivated", "disabled"];
|
|
24430
25342
|
function storePath3(dataDir) {
|
|
24431
|
-
return
|
|
25343
|
+
return path35.join(dataDir, "users.json");
|
|
24432
25344
|
}
|
|
24433
25345
|
function loadStore(dataDir) {
|
|
24434
25346
|
const p = storePath3(dataDir);
|
|
24435
25347
|
let raw;
|
|
24436
25348
|
try {
|
|
24437
|
-
raw =
|
|
25349
|
+
raw = fs26.readFileSync(p, "utf8");
|
|
24438
25350
|
} catch (err) {
|
|
24439
25351
|
if (err.code === "ENOENT") return [];
|
|
24440
25352
|
throw new Error(`Cannot read hosted-user store at ${p}: ${err.message}`);
|
|
@@ -24452,10 +25364,10 @@ function loadStore(dataDir) {
|
|
|
24452
25364
|
}
|
|
24453
25365
|
function replaceAll(dataDir, records) {
|
|
24454
25366
|
const p = storePath3(dataDir);
|
|
24455
|
-
|
|
25367
|
+
fs26.mkdirSync(path35.dirname(p), { recursive: true });
|
|
24456
25368
|
const tmp = `${p}.tmp`;
|
|
24457
|
-
|
|
24458
|
-
|
|
25369
|
+
fs26.writeFileSync(tmp, JSON.stringify(records, null, 2) + "\n");
|
|
25370
|
+
fs26.renameSync(tmp, p);
|
|
24459
25371
|
}
|
|
24460
25372
|
function registryUpsert(dataDir, record2) {
|
|
24461
25373
|
const records = loadStore(dataDir);
|
|
@@ -24557,11 +25469,11 @@ function remapUnitReferences(dataDir, remap, removedScopeIds) {
|
|
|
24557
25469
|
}
|
|
24558
25470
|
|
|
24559
25471
|
// src/server/instance.ts
|
|
24560
|
-
var
|
|
24561
|
-
var
|
|
25472
|
+
var fs27 = __toESM(require("fs"));
|
|
25473
|
+
var path36 = __toESM(require("path"));
|
|
24562
25474
|
var import_crypto = require("crypto");
|
|
24563
25475
|
function storePath4(dataDir) {
|
|
24564
|
-
return
|
|
25476
|
+
return path36.join(dataDir, "instance.json");
|
|
24565
25477
|
}
|
|
24566
25478
|
var InstanceIdentityStore = class {
|
|
24567
25479
|
constructor(dataDir) {
|
|
@@ -24578,7 +25490,7 @@ var InstanceIdentityStore = class {
|
|
|
24578
25490
|
const p = storePath4(this.dataDir);
|
|
24579
25491
|
let raw;
|
|
24580
25492
|
try {
|
|
24581
|
-
raw =
|
|
25493
|
+
raw = fs27.readFileSync(p, "utf8");
|
|
24582
25494
|
} catch (err) {
|
|
24583
25495
|
if (err.code === "ENOENT") return null;
|
|
24584
25496
|
throw new Error(`Cannot read instance identity at ${p}: ${err.message}`);
|
|
@@ -24601,10 +25513,10 @@ var InstanceIdentityStore = class {
|
|
|
24601
25513
|
* never truncates the file. Only called by the registry's create-once seed. */
|
|
24602
25514
|
replace(identity) {
|
|
24603
25515
|
const p = storePath4(this.dataDir);
|
|
24604
|
-
|
|
25516
|
+
fs27.mkdirSync(path36.dirname(p), { recursive: true });
|
|
24605
25517
|
const tmp = `${p}.tmp`;
|
|
24606
|
-
|
|
24607
|
-
|
|
25518
|
+
fs27.writeFileSync(tmp, JSON.stringify(identity, null, 2) + "\n");
|
|
25519
|
+
fs27.renameSync(tmp, p);
|
|
24608
25520
|
}
|
|
24609
25521
|
};
|
|
24610
25522
|
var InstanceIdentityRegistry = class {
|
|
@@ -24660,8 +25572,8 @@ function getInstanceIdentity(dataDir) {
|
|
|
24660
25572
|
}
|
|
24661
25573
|
|
|
24662
25574
|
// src/utils/secrets.ts
|
|
24663
|
-
var
|
|
24664
|
-
var
|
|
25575
|
+
var fs28 = __toESM(require("fs"));
|
|
25576
|
+
var path37 = __toESM(require("path"));
|
|
24665
25577
|
var ENV_FALLBACK = {
|
|
24666
25578
|
"git-token": ["WAIRON_GIT_TOKEN"],
|
|
24667
25579
|
"notion-token": ["WAIRON_NOTION_TOKEN"],
|
|
@@ -24670,13 +25582,13 @@ var ENV_FALLBACK = {
|
|
|
24670
25582
|
};
|
|
24671
25583
|
function storePath5() {
|
|
24672
25584
|
const dataDir = process.env["WAIRON_DATA_DIR"];
|
|
24673
|
-
return dataDir ?
|
|
25585
|
+
return dataDir ? path37.join(dataDir, "auth", "secrets.json") : null;
|
|
24674
25586
|
}
|
|
24675
25587
|
function readStore() {
|
|
24676
25588
|
const p = storePath5();
|
|
24677
25589
|
if (!p) return {};
|
|
24678
25590
|
try {
|
|
24679
|
-
return JSON.parse(
|
|
25591
|
+
return JSON.parse(fs28.readFileSync(p, "utf8"));
|
|
24680
25592
|
} catch {
|
|
24681
25593
|
return {};
|
|
24682
25594
|
}
|
|
@@ -24701,10 +25613,10 @@ function setSecret(key, value) {
|
|
|
24701
25613
|
if (!p) throw new Error("WAIRON_DATA_DIR is not set \u2014 a running server needs it to store secrets.");
|
|
24702
25614
|
const store = readStore();
|
|
24703
25615
|
store[key] = value;
|
|
24704
|
-
|
|
25616
|
+
fs28.mkdirSync(path37.dirname(p), { recursive: true });
|
|
24705
25617
|
const tmp = `${p}.tmp`;
|
|
24706
|
-
|
|
24707
|
-
|
|
25618
|
+
fs28.writeFileSync(tmp, JSON.stringify(store, null, 2) + "\n");
|
|
25619
|
+
fs28.renameSync(tmp, p);
|
|
24708
25620
|
}
|
|
24709
25621
|
function listSecretKeys() {
|
|
24710
25622
|
return Object.keys(readStore());
|
|
@@ -24912,17 +25824,17 @@ function verifySsoState(state) {
|
|
|
24912
25824
|
}
|
|
24913
25825
|
|
|
24914
25826
|
// src/server/organization.ts
|
|
24915
|
-
var
|
|
24916
|
-
var
|
|
25827
|
+
var fs29 = __toESM(require("fs"));
|
|
25828
|
+
var path38 = __toESM(require("path"));
|
|
24917
25829
|
var crypto6 = __toESM(require("crypto"));
|
|
24918
25830
|
function storePath6(dataDir) {
|
|
24919
|
-
return
|
|
25831
|
+
return path38.join(dataDir, "organization.json");
|
|
24920
25832
|
}
|
|
24921
25833
|
function readState(dataDir) {
|
|
24922
25834
|
const p = storePath6(dataDir);
|
|
24923
25835
|
let raw;
|
|
24924
25836
|
try {
|
|
24925
|
-
raw =
|
|
25837
|
+
raw = fs29.readFileSync(p, "utf8");
|
|
24926
25838
|
} catch (e) {
|
|
24927
25839
|
if (e.code === "ENOENT") return { units: [], placements: [] };
|
|
24928
25840
|
throw new Error(`Failed to read organization store at ${p}: ${e.message}`);
|
|
@@ -24939,10 +25851,10 @@ function readState(dataDir) {
|
|
|
24939
25851
|
}
|
|
24940
25852
|
function persistState(dataDir, state) {
|
|
24941
25853
|
const p = storePath6(dataDir);
|
|
24942
|
-
|
|
25854
|
+
fs29.mkdirSync(path38.dirname(p), { recursive: true });
|
|
24943
25855
|
const tmp = `${p}.tmp`;
|
|
24944
|
-
|
|
24945
|
-
|
|
25856
|
+
fs29.writeFileSync(tmp, JSON.stringify(state, null, 2) + "\n");
|
|
25857
|
+
fs29.renameSync(tmp, p);
|
|
24946
25858
|
}
|
|
24947
25859
|
var SLUG_PATTERN = /^[a-z0-9-]+$/;
|
|
24948
25860
|
var UNIT_KINDS = ["business_entity", "department", "team", "group"];
|
|
@@ -25311,11 +26223,11 @@ function getOrganizationUnit(dataDir, id) {
|
|
|
25311
26223
|
}
|
|
25312
26224
|
|
|
25313
26225
|
// src/server/permissions.ts
|
|
25314
|
-
var
|
|
25315
|
-
var
|
|
26226
|
+
var fs30 = __toESM(require("fs"));
|
|
26227
|
+
var path39 = __toESM(require("path"));
|
|
25316
26228
|
var import_crypto2 = require("crypto");
|
|
25317
26229
|
function storePath7(dataDir) {
|
|
25318
|
-
return
|
|
26230
|
+
return path39.join(dataDir, "permissions.json");
|
|
25319
26231
|
}
|
|
25320
26232
|
function assignmentKey(a) {
|
|
25321
26233
|
return [a.subjectKind, a.subjectId ?? "", a.scopeKind, a.scopeId ?? "", a.capability].join("|");
|
|
@@ -25324,7 +26236,7 @@ function load4(dataDir) {
|
|
|
25324
26236
|
const p = storePath7(dataDir);
|
|
25325
26237
|
let raw;
|
|
25326
26238
|
try {
|
|
25327
|
-
raw =
|
|
26239
|
+
raw = fs30.readFileSync(p, "utf8");
|
|
25328
26240
|
} catch (err) {
|
|
25329
26241
|
if (err.code === "ENOENT") return [];
|
|
25330
26242
|
throw new Error(`Cannot read permission store at ${p}: ${err.message}`);
|
|
@@ -25342,10 +26254,10 @@ function load4(dataDir) {
|
|
|
25342
26254
|
}
|
|
25343
26255
|
function replaceAll2(dataDir, assignments) {
|
|
25344
26256
|
const p = storePath7(dataDir);
|
|
25345
|
-
|
|
26257
|
+
fs30.mkdirSync(path39.dirname(p), { recursive: true });
|
|
25346
26258
|
const tmp = `${p}.tmp`;
|
|
25347
|
-
|
|
25348
|
-
|
|
26259
|
+
fs30.writeFileSync(tmp, JSON.stringify(assignments, null, 2) + "\n");
|
|
26260
|
+
fs30.renameSync(tmp, p);
|
|
25349
26261
|
}
|
|
25350
26262
|
function registrySet(dataDir, assignment) {
|
|
25351
26263
|
const assignments = load4(dataDir);
|
|
@@ -25431,8 +26343,8 @@ function getAssignment(dataDir, assignmentId) {
|
|
|
25431
26343
|
}
|
|
25432
26344
|
|
|
25433
26345
|
// src/server/roles.ts
|
|
25434
|
-
var
|
|
25435
|
-
var
|
|
26346
|
+
var fs31 = __toESM(require("fs"));
|
|
26347
|
+
var path40 = __toESM(require("path"));
|
|
25436
26348
|
var BUILTIN_ROLES = [
|
|
25437
26349
|
{
|
|
25438
26350
|
id: SSO_ADMIN_ROLE_ID,
|
|
@@ -25450,13 +26362,13 @@ function isBuiltinRoleId(roleId) {
|
|
|
25450
26362
|
return BUILTIN_ROLE_IDS.has(roleId);
|
|
25451
26363
|
}
|
|
25452
26364
|
function storePath8(dataDir) {
|
|
25453
|
-
return
|
|
26365
|
+
return path40.join(dataDir, "roles.json");
|
|
25454
26366
|
}
|
|
25455
26367
|
function load5(dataDir) {
|
|
25456
26368
|
const p = storePath8(dataDir);
|
|
25457
26369
|
let raw;
|
|
25458
26370
|
try {
|
|
25459
|
-
raw =
|
|
26371
|
+
raw = fs31.readFileSync(p, "utf8");
|
|
25460
26372
|
} catch (err) {
|
|
25461
26373
|
if (err.code === "ENOENT") return [];
|
|
25462
26374
|
throw new Error(`Cannot read role store at ${p}: ${err.message}`);
|
|
@@ -25474,10 +26386,10 @@ function load5(dataDir) {
|
|
|
25474
26386
|
}
|
|
25475
26387
|
function replaceAll3(dataDir, roles) {
|
|
25476
26388
|
const p = storePath8(dataDir);
|
|
25477
|
-
|
|
26389
|
+
fs31.mkdirSync(path40.dirname(p), { recursive: true });
|
|
25478
26390
|
const tmp = `${p}.tmp`;
|
|
25479
|
-
|
|
25480
|
-
|
|
26391
|
+
fs31.writeFileSync(tmp, JSON.stringify(roles, null, 2) + "\n");
|
|
26392
|
+
fs31.renameSync(tmp, p);
|
|
25481
26393
|
}
|
|
25482
26394
|
function registryCreate(dataDir, role) {
|
|
25483
26395
|
if (isBuiltinRoleId(role.id)) {
|
|
@@ -25711,131 +26623,14 @@ function actionableUnitIds(scopes) {
|
|
|
25711
26623
|
}
|
|
25712
26624
|
|
|
25713
26625
|
// src/server/projects.ts
|
|
25714
|
-
var
|
|
25715
|
-
var
|
|
25716
|
-
|
|
25717
|
-
|
|
25718
|
-
return typeof id === "string" && ID_RE.test(id);
|
|
25719
|
-
}
|
|
25720
|
-
function registryPath(dataDir) {
|
|
25721
|
-
return path40.join(dataDir, "projects.json");
|
|
25722
|
-
}
|
|
25723
|
-
function load6(dataDir) {
|
|
25724
|
-
try {
|
|
25725
|
-
return JSON.parse(fs31.readFileSync(registryPath(dataDir), "utf8"));
|
|
25726
|
-
} catch {
|
|
25727
|
-
return [];
|
|
25728
|
-
}
|
|
25729
|
-
}
|
|
25730
|
-
function save2(dataDir, records) {
|
|
25731
|
-
const p = registryPath(dataDir);
|
|
25732
|
-
fs31.mkdirSync(path40.dirname(p), { recursive: true });
|
|
25733
|
-
const tmp = `${p}.tmp`;
|
|
25734
|
-
fs31.writeFileSync(tmp, JSON.stringify(records, null, 2) + "\n");
|
|
25735
|
-
fs31.renameSync(tmp, p);
|
|
25736
|
-
}
|
|
25737
|
-
function projectRoot(dataDir, id) {
|
|
25738
|
-
return path40.join(dataDir, "projects", id);
|
|
25739
|
-
}
|
|
25740
|
-
function existingProjectRoot(dataDir, id) {
|
|
25741
|
-
if (!isValidProjectId(id)) return null;
|
|
25742
|
-
const rec = load6(dataDir).find((r) => r.id === id);
|
|
25743
|
-
return rec ? rec.rootPath : null;
|
|
25744
|
-
}
|
|
25745
|
-
function createProjectRecord(dataDir, id) {
|
|
25746
|
-
if (!isValidProjectId(id)) {
|
|
25747
|
-
throw new Error(`Invalid project id "${id}" (allowed: lowercase letters, digits, hyphen).`);
|
|
25748
|
-
}
|
|
25749
|
-
const records = load6(dataDir);
|
|
25750
|
-
if (records.some((r) => r.id === id)) {
|
|
25751
|
-
throw new Error(`Project "${id}" already exists.`);
|
|
25752
|
-
}
|
|
25753
|
-
const root = projectRoot(dataDir, id);
|
|
25754
|
-
fs31.mkdirSync(root, { recursive: true });
|
|
25755
|
-
const record2 = {
|
|
25756
|
-
id,
|
|
25757
|
-
rootPath: root,
|
|
25758
|
-
status: "active",
|
|
25759
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
25760
|
-
};
|
|
25761
|
-
records.push(record2);
|
|
25762
|
-
save2(dataDir, records);
|
|
25763
|
-
return record2;
|
|
25764
|
-
}
|
|
25765
|
-
function registerLocalDevProject(dataDir, id, rootPath) {
|
|
25766
|
-
if (!isValidProjectId(id)) {
|
|
25767
|
-
throw new Error(`Invalid project id "${id}" (allowed: lowercase letters, digits, hyphen).`);
|
|
25768
|
-
}
|
|
25769
|
-
const records = load6(dataDir);
|
|
25770
|
-
const existing = records.find((r) => r.id === id);
|
|
25771
|
-
const record2 = {
|
|
25772
|
-
id,
|
|
25773
|
-
rootPath,
|
|
25774
|
-
status: "active",
|
|
25775
|
-
createdAt: existing?.createdAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
25776
|
-
};
|
|
25777
|
-
const next = existing ? records.map((r) => r.id === id ? record2 : r) : [...records, record2];
|
|
25778
|
-
save2(dataDir, next);
|
|
25779
|
-
return record2;
|
|
25780
|
-
}
|
|
25781
|
-
function listProjectRecords(dataDir) {
|
|
25782
|
-
return load6(dataDir);
|
|
25783
|
-
}
|
|
25784
|
-
function removeProjectRecord(dataDir, id) {
|
|
25785
|
-
const records = load6(dataDir);
|
|
25786
|
-
const rec = records.find((r) => r.id === id);
|
|
25787
|
-
if (rec) {
|
|
25788
|
-
try {
|
|
25789
|
-
fs31.rmSync(rec.rootPath, { recursive: true, force: true });
|
|
25790
|
-
} catch {
|
|
25791
|
-
}
|
|
25792
|
-
}
|
|
25793
|
-
save2(dataDir, records.filter((r) => r.id !== id));
|
|
25794
|
-
}
|
|
25795
|
-
function resolveProjectRoot(dataDir, principal, selector) {
|
|
25796
|
-
const authorized = principal.projects;
|
|
25797
|
-
const wildcard = authorized.includes("*");
|
|
25798
|
-
let target;
|
|
25799
|
-
if (selector) {
|
|
25800
|
-
if (!wildcard && !authorized.includes(selector)) return null;
|
|
25801
|
-
target = selector;
|
|
25802
|
-
} else if (!wildcard && authorized.length === 1) {
|
|
25803
|
-
target = authorized[0];
|
|
25804
|
-
} else {
|
|
25805
|
-
return null;
|
|
25806
|
-
}
|
|
25807
|
-
if (!isValidProjectId(target)) return null;
|
|
25808
|
-
const rec = load6(dataDir).find((r) => r.id === target);
|
|
25809
|
-
if (!rec || rec.status !== "active") return null;
|
|
25810
|
-
return rec.rootPath;
|
|
25811
|
-
}
|
|
25812
|
-
|
|
25813
|
-
// src/server/adapters.ts
|
|
25814
|
-
init_statehash();
|
|
25815
|
-
|
|
25816
|
-
// src/core/lockfile.ts
|
|
25817
|
-
var fs32 = __toESM(require("fs"));
|
|
25818
|
-
var path41 = __toESM(require("path"));
|
|
26626
|
+
var fs35 = __toESM(require("fs"));
|
|
26627
|
+
var path44 = __toESM(require("path"));
|
|
26628
|
+
init_loader();
|
|
26629
|
+
init_yaml();
|
|
25819
26630
|
init_fs();
|
|
25820
|
-
function lockPath() {
|
|
25821
|
-
return aiDir("lock.json");
|
|
25822
|
-
}
|
|
25823
|
-
function readLockRecord() {
|
|
25824
|
-
try {
|
|
25825
|
-
return JSON.parse(fs32.readFileSync(lockPath(), "utf8"));
|
|
25826
|
-
} catch {
|
|
25827
|
-
return null;
|
|
25828
|
-
}
|
|
25829
|
-
}
|
|
25830
|
-
function writeLockRecord(record2) {
|
|
25831
|
-
const p = lockPath();
|
|
25832
|
-
fs32.mkdirSync(path41.dirname(p), { recursive: true });
|
|
25833
|
-
const tmp = `${p}.tmp`;
|
|
25834
|
-
fs32.writeFileSync(tmp, JSON.stringify(record2, null, 2) + "\n");
|
|
25835
|
-
fs32.renameSync(tmp, p);
|
|
25836
|
-
}
|
|
25837
26631
|
|
|
25838
26632
|
// src/server/adapters.ts
|
|
26633
|
+
init_statehash();
|
|
25839
26634
|
init_specs2();
|
|
25840
26635
|
init_provision();
|
|
25841
26636
|
init_validation();
|
|
@@ -25846,37 +26641,37 @@ init_types();
|
|
|
25846
26641
|
init_server();
|
|
25847
26642
|
|
|
25848
26643
|
// src/git/config.ts
|
|
25849
|
-
var
|
|
25850
|
-
var
|
|
26644
|
+
var fs32 = __toESM(require("fs"));
|
|
26645
|
+
var path41 = __toESM(require("path"));
|
|
25851
26646
|
init_fs();
|
|
25852
26647
|
function configPath() {
|
|
25853
26648
|
return aiDir("git.json");
|
|
25854
26649
|
}
|
|
25855
26650
|
function readGitConfig() {
|
|
25856
26651
|
try {
|
|
25857
|
-
return JSON.parse(
|
|
26652
|
+
return JSON.parse(fs32.readFileSync(configPath(), "utf8"));
|
|
25858
26653
|
} catch {
|
|
25859
26654
|
return null;
|
|
25860
26655
|
}
|
|
25861
26656
|
}
|
|
25862
26657
|
function writeGitConfig(config) {
|
|
25863
26658
|
const p = configPath();
|
|
25864
|
-
|
|
26659
|
+
fs32.mkdirSync(path41.dirname(p), { recursive: true });
|
|
25865
26660
|
const tmp = `${p}.tmp`;
|
|
25866
|
-
|
|
25867
|
-
|
|
26661
|
+
fs32.writeFileSync(tmp, JSON.stringify(config, null, 2) + "\n");
|
|
26662
|
+
fs32.renameSync(tmp, p);
|
|
25868
26663
|
}
|
|
25869
26664
|
function clearGitConfig() {
|
|
25870
26665
|
try {
|
|
25871
|
-
|
|
26666
|
+
fs32.rmSync(configPath(), { force: true });
|
|
25872
26667
|
} catch {
|
|
25873
26668
|
}
|
|
25874
26669
|
}
|
|
25875
26670
|
|
|
25876
26671
|
// src/git/adapter.ts
|
|
25877
26672
|
var import_child_process3 = require("child_process");
|
|
25878
|
-
var
|
|
25879
|
-
var
|
|
26673
|
+
var fs33 = __toESM(require("fs"));
|
|
26674
|
+
var path42 = __toESM(require("path"));
|
|
25880
26675
|
init_fs();
|
|
25881
26676
|
function git(args, cwd) {
|
|
25882
26677
|
return (0, import_child_process3.execFileSync)("git", args, {
|
|
@@ -25928,10 +26723,10 @@ function compareUrl(remote, defaultBranch, workingBranch) {
|
|
|
25928
26723
|
return `${web}/compare/${encodeURIComponent(defaultBranch)}...${encodeURIComponent(workingBranch)}`;
|
|
25929
26724
|
}
|
|
25930
26725
|
function excludeLocalFiles() {
|
|
25931
|
-
const excludePath =
|
|
26726
|
+
const excludePath = path42.join(getProjectRoot(), ".git", "info", "exclude");
|
|
25932
26727
|
try {
|
|
25933
|
-
|
|
25934
|
-
|
|
26728
|
+
fs33.mkdirSync(path42.dirname(excludePath), { recursive: true });
|
|
26729
|
+
fs33.appendFileSync(excludePath, "\n.wai/lock.json\n.wai/git.json\n");
|
|
25935
26730
|
} catch {
|
|
25936
26731
|
}
|
|
25937
26732
|
}
|
|
@@ -25996,39 +26791,39 @@ function configureSync(periodicSyncMinutes, skipIfClean) {
|
|
|
25996
26791
|
}
|
|
25997
26792
|
|
|
25998
26793
|
// src/producers/config.ts
|
|
25999
|
-
var
|
|
26000
|
-
var
|
|
26794
|
+
var fs34 = __toESM(require("fs"));
|
|
26795
|
+
var path43 = __toESM(require("path"));
|
|
26001
26796
|
init_fs();
|
|
26002
26797
|
function configPath2() {
|
|
26003
26798
|
return aiDir("producers.json");
|
|
26004
26799
|
}
|
|
26005
|
-
function
|
|
26800
|
+
function load6() {
|
|
26006
26801
|
try {
|
|
26007
|
-
return JSON.parse(
|
|
26802
|
+
return JSON.parse(fs34.readFileSync(configPath2(), "utf8"));
|
|
26008
26803
|
} catch {
|
|
26009
26804
|
return [];
|
|
26010
26805
|
}
|
|
26011
26806
|
}
|
|
26012
|
-
function
|
|
26807
|
+
function save2(configs) {
|
|
26013
26808
|
const p = configPath2();
|
|
26014
|
-
|
|
26809
|
+
fs34.mkdirSync(path43.dirname(p), { recursive: true });
|
|
26015
26810
|
const tmp = `${p}.tmp`;
|
|
26016
|
-
|
|
26017
|
-
|
|
26811
|
+
fs34.writeFileSync(tmp, JSON.stringify(configs, null, 2) + "\n");
|
|
26812
|
+
fs34.renameSync(tmp, p);
|
|
26018
26813
|
}
|
|
26019
26814
|
function readProducerConfig(target) {
|
|
26020
|
-
return
|
|
26815
|
+
return load6().find((c) => c.target === target) ?? null;
|
|
26021
26816
|
}
|
|
26022
26817
|
function writeProducerConfig(config) {
|
|
26023
|
-
const configs =
|
|
26818
|
+
const configs = load6().filter((c) => c.target !== config.target);
|
|
26024
26819
|
configs.push(config);
|
|
26025
|
-
|
|
26820
|
+
save2(configs);
|
|
26026
26821
|
}
|
|
26027
26822
|
function clearProducerConfig(target) {
|
|
26028
|
-
|
|
26823
|
+
save2(load6().filter((c) => c.target !== target));
|
|
26029
26824
|
}
|
|
26030
26825
|
function listProducerConfigs() {
|
|
26031
|
-
return
|
|
26826
|
+
return load6();
|
|
26032
26827
|
}
|
|
26033
26828
|
|
|
26034
26829
|
// src/producers/core-adapter.ts
|
|
@@ -26367,8 +27162,19 @@ var hostCore = {
|
|
|
26367
27162
|
/** The ids of wairon's built-in architectural profiles, read from the core rules
|
|
26368
27163
|
* registry's built-in profile set (BUILTIN_PROFILES) — a pure, side-effect-free
|
|
26369
27164
|
* read of a bundled constant. */
|
|
26370
|
-
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]
|
|
26371
27174
|
};
|
|
27175
|
+
function resolveContainedProjectPath(projectRoot2, projectPath) {
|
|
27176
|
+
return assertContainedProjectPath(projectRoot2, projectPath);
|
|
27177
|
+
}
|
|
26372
27178
|
function validateProjectAsComplete() {
|
|
26373
27179
|
const config = loadProjectConfig();
|
|
26374
27180
|
return validateAsComplete({ rules: config.rules, projectType: config.projectType });
|
|
@@ -26400,6 +27206,180 @@ var hostSdk = {
|
|
|
26400
27206
|
extractArchive: (archive, destDir, limits) => sdkPortal.extractPack(archive, destDir, limits)
|
|
26401
27207
|
};
|
|
26402
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
|
+
|
|
26403
27383
|
// src/server/errors.ts
|
|
26404
27384
|
var UnauthenticatedError = class extends Error {
|
|
26405
27385
|
constructor() {
|
|
@@ -26520,9 +27500,14 @@ function lockProject(cfg, credential, project2) {
|
|
|
26520
27500
|
}
|
|
26521
27501
|
return executeApprovedLock(cfg, project2);
|
|
26522
27502
|
}
|
|
26523
|
-
function
|
|
27503
|
+
function boundLifecycleRoot(cfg, projectId, subproject) {
|
|
26524
27504
|
const root = existingProjectRoot(cfg.dataDir, projectId);
|
|
26525
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);
|
|
26526
27511
|
return runWithProjectRoot(root, () => {
|
|
26527
27512
|
hostGit.sync();
|
|
26528
27513
|
const result = validateProjectAsComplete();
|
|
@@ -26694,9 +27679,8 @@ function promoteProject(cfg, credential, project2) {
|
|
|
26694
27679
|
}
|
|
26695
27680
|
return executeApprovedPromote(cfg, project2);
|
|
26696
27681
|
}
|
|
26697
|
-
function executeApprovedPromote(cfg, projectId) {
|
|
26698
|
-
const root =
|
|
26699
|
-
if (!root) throw new Error(`Unknown project "${projectId}".`);
|
|
27682
|
+
function executeApprovedPromote(cfg, projectId, subproject) {
|
|
27683
|
+
const root = boundLifecycleRoot(cfg, projectId, subproject);
|
|
26700
27684
|
return runWithProjectRoot(root, () => {
|
|
26701
27685
|
const lock = hostCore.readLockRecord();
|
|
26702
27686
|
if (!lock) {
|
|
@@ -26811,6 +27795,35 @@ function storeListGlobalPacks() {
|
|
|
26811
27795
|
);
|
|
26812
27796
|
return [...instance, ...image];
|
|
26813
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
|
+
}
|
|
26814
27827
|
function storeListAvailableProfiles() {
|
|
26815
27828
|
const out = [];
|
|
26816
27829
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -26821,19 +27834,21 @@ function storeListAvailableProfiles() {
|
|
|
26821
27834
|
out.push(family ? { id, source, family } : { id, source });
|
|
26822
27835
|
};
|
|
26823
27836
|
for (const id of hostCore.builtinProfileIds()) emit(id, "builtin");
|
|
26824
|
-
const
|
|
26825
|
-
|
|
26826
|
-
|
|
26827
|
-
|
|
26828
|
-
|
|
26829
|
-
|
|
26830
|
-
|
|
26831
|
-
|
|
26832
|
-
|
|
26833
|
-
|
|
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 });
|
|
26834
27848
|
};
|
|
26835
|
-
|
|
26836
|
-
|
|
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);
|
|
26837
27852
|
return out;
|
|
26838
27853
|
}
|
|
26839
27854
|
function readPackContent(full) {
|
|
@@ -27026,6 +28041,10 @@ function listAvailableProfiles(cfg, credential) {
|
|
|
27026
28041
|
requirePrincipal2(cfg, credential);
|
|
27027
28042
|
return storeListAvailableProfiles();
|
|
27028
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
|
+
}
|
|
27029
28048
|
function listAdoptableProjectPacks(cfg, credential, project2) {
|
|
27030
28049
|
requireCap(cfg, credential, "project:read", "project", project2, "Forbidden \u2014 listing adoptable packs requires project:read over the project");
|
|
27031
28050
|
return storeListGlobalPacks();
|
|
@@ -27048,6 +28067,26 @@ function executeApprovedInstallProjectPack(cfg, project2, name, content) {
|
|
|
27048
28067
|
function executeApprovedResolveGlobalPacks(names) {
|
|
27049
28068
|
return storeResolveGlobalPacks(names);
|
|
27050
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
|
+
}
|
|
27051
28090
|
function removeProjectPack(cfg, credential, project2, name) {
|
|
27052
28091
|
requireCap(cfg, credential, "project:admin", "project", project2, "Forbidden \u2014 removing a project pack requires project:admin over the project");
|
|
27053
28092
|
runWithProjectRoot(boundProject2(cfg, project2), () => storeRemoveProjectPack(name));
|
|
@@ -28042,6 +29081,40 @@ function requestPackNames(request) {
|
|
|
28042
29081
|
if (!sel) return [];
|
|
28043
29082
|
return [.../* @__PURE__ */ new Set([...sel.requiredPackNames ?? [], ...sel.defaultPackNames ?? []])];
|
|
28044
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
|
+
}
|
|
28045
29118
|
function resolvedSelection(request, policy, selectedBy) {
|
|
28046
29119
|
const sel = request.profileSelection;
|
|
28047
29120
|
const selection = {
|
|
@@ -28060,7 +29133,8 @@ function buildEvaluation(input) {
|
|
|
28060
29133
|
selectedProfileIds,
|
|
28061
29134
|
hasSelection,
|
|
28062
29135
|
countMissingPacksAsViolation,
|
|
28063
|
-
requiredDefaultResolution
|
|
29136
|
+
requiredDefaultResolution,
|
|
29137
|
+
governingProfileId
|
|
28064
29138
|
} = input;
|
|
28065
29139
|
const present = new Set(presentPackNames);
|
|
28066
29140
|
let missingPackNames;
|
|
@@ -28081,6 +29155,7 @@ function buildEvaluation(input) {
|
|
|
28081
29155
|
const allowed = policy.allowedProfileIds;
|
|
28082
29156
|
const disallowedProfileIds = allowed && allowed.length > 0 ? selectedProfileIds.filter((id) => !allowed.includes(id)) : [];
|
|
28083
29157
|
const selectionRequiredUnmet = policy.requireProfileSelection && !hasSelection;
|
|
29158
|
+
const unappliedProfileIds = governingProfileId ? unappliedIds(selectedProfileIds, governingProfileId) : [];
|
|
28084
29159
|
const messages = [];
|
|
28085
29160
|
if (selectionRequiredUnmet) {
|
|
28086
29161
|
messages.push("Profile selection is required by policy but none was provided.");
|
|
@@ -28092,16 +29167,24 @@ function buildEvaluation(input) {
|
|
|
28092
29167
|
for (const n of blockedPackNames) messages.push(`Pack "${n}" is blocked by policy.`);
|
|
28093
29168
|
for (const id of missingProfileIds) messages.push(`Required profile "${id}" is not selected.`);
|
|
28094
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
|
+
}
|
|
28095
29175
|
const violation = selectionRequiredUnmet || blockedPackNames.length > 0 || missingProfileIds.length > 0 || disallowedProfileIds.length > 0 || countMissingPacksAsViolation && (missingPackNames.length > 0 || unresolvedPacks.length > 0);
|
|
28096
|
-
|
|
29176
|
+
const result = {
|
|
28097
29177
|
compliant: !violation,
|
|
28098
29178
|
mode: policy.enforcementMode,
|
|
28099
29179
|
missingPackNames,
|
|
28100
29180
|
blockedPackNames,
|
|
28101
29181
|
missingProfileIds,
|
|
28102
29182
|
unresolvedPacks,
|
|
29183
|
+
unappliedProfileIds,
|
|
28103
29184
|
messages
|
|
28104
29185
|
};
|
|
29186
|
+
if (governingProfileId) result.governingProfileId = governingProfileId;
|
|
29187
|
+
return result;
|
|
28105
29188
|
}
|
|
28106
29189
|
function performInit(cfg, request, principal) {
|
|
28107
29190
|
const policy = effectivePolicy(cfg.dataDir);
|
|
@@ -28121,17 +29204,24 @@ function performInit(cfg, request, principal) {
|
|
|
28121
29204
|
request.ownerUnitId,
|
|
28122
29205
|
principal ? principalSubject3(principal) : void 0
|
|
28123
29206
|
);
|
|
28124
|
-
|
|
28125
|
-
|
|
28126
|
-
|
|
28127
|
-
|
|
28128
|
-
|
|
28129
|
-
|
|
28130
|
-
...requestPackNames(request)
|
|
28131
|
-
])
|
|
28132
|
-
);
|
|
29207
|
+
const packResolution = executeApprovedResolveGlobalPacks([
|
|
29208
|
+
...policy.requiredGlobalPacks,
|
|
29209
|
+
...policy.defaultProjectPacks,
|
|
29210
|
+
...requestPackNames(request)
|
|
29211
|
+
]);
|
|
29212
|
+
installResolvedPacks(cfg, record2.id, packResolution);
|
|
28133
29213
|
const selectedBy = principal ? principalSubject3(principal) : void 0;
|
|
28134
|
-
|
|
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];
|
|
28135
29225
|
const actor = principal ? principalSubject3(principal) : SYSTEM_SUBJECT;
|
|
28136
29226
|
tryAppendAudit2(
|
|
28137
29227
|
cfg,
|
|
@@ -28140,7 +29230,18 @@ function performInit(cfg, request, principal) {
|
|
|
28140
29230
|
"project.init.policy",
|
|
28141
29231
|
"info",
|
|
28142
29232
|
"project",
|
|
28143
|
-
{
|
|
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
|
+
},
|
|
28144
29245
|
principal?.tokenId
|
|
28145
29246
|
)
|
|
28146
29247
|
);
|
|
@@ -28181,6 +29282,7 @@ function evaluateProjectPolicy(cfg, credential, projectId) {
|
|
|
28181
29282
|
const root = resolveProjectRoot(cfg.dataDir, principal, projectId);
|
|
28182
29283
|
if (!root) throw new Error(`Unknown project "${projectId}".`);
|
|
28183
29284
|
const policy = effectivePolicy(cfg.dataDir);
|
|
29285
|
+
const governingProfileId = readProjectType(root);
|
|
28184
29286
|
const selection = readProjectProfileSelection(root);
|
|
28185
29287
|
return buildEvaluation({
|
|
28186
29288
|
policy,
|
|
@@ -28188,7 +29290,8 @@ function evaluateProjectPolicy(cfg, credential, projectId) {
|
|
|
28188
29290
|
selectedProfileIds: selection?.profileIds ?? [],
|
|
28189
29291
|
hasSelection: !!selection,
|
|
28190
29292
|
countMissingPacksAsViolation: true,
|
|
28191
|
-
requiredDefaultResolution: executeApprovedResolveGlobalPacks(requiredDefaultNames(policy))
|
|
29293
|
+
requiredDefaultResolution: executeApprovedResolveGlobalPacks(requiredDefaultNames(policy)),
|
|
29294
|
+
governingProfileId
|
|
28192
29295
|
});
|
|
28193
29296
|
}
|
|
28194
29297
|
function reconcileProjectPolicy(cfg, credential, projectId) {
|
|
@@ -28203,20 +29306,41 @@ function reconcileProjectPolicy(cfg, credential, projectId) {
|
|
|
28203
29306
|
const policy = effectivePolicy(cfg.dataDir);
|
|
28204
29307
|
const selection = readProjectProfileSelection(root);
|
|
28205
29308
|
const resolution = executeApprovedResolveGlobalPacks(requiredDefaultNames(policy));
|
|
28206
|
-
|
|
28207
|
-
const installedSet = new Set(installed);
|
|
29309
|
+
const installedSet = new Set(installedPackNames(cfg, projectId));
|
|
28208
29310
|
const toApply = resolution.resolved.filter((p) => !installedSet.has(p.name));
|
|
29311
|
+
const appliedPackNames = toApply.map((p) => p.name);
|
|
28209
29312
|
if (toApply.length > 0) {
|
|
28210
29313
|
installResolvedPacks(cfg, projectId, { resolved: toApply, unresolved: [] });
|
|
28211
|
-
|
|
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
|
+
}
|
|
28212
29335
|
}
|
|
28213
29336
|
const result = buildEvaluation({
|
|
28214
29337
|
policy,
|
|
28215
|
-
presentPackNames:
|
|
28216
|
-
selectedProfileIds
|
|
29338
|
+
presentPackNames: installedPackNames(cfg, projectId),
|
|
29339
|
+
selectedProfileIds,
|
|
28217
29340
|
hasSelection: !!selection,
|
|
28218
29341
|
countMissingPacksAsViolation: true,
|
|
28219
|
-
requiredDefaultResolution: resolution
|
|
29342
|
+
requiredDefaultResolution: resolution,
|
|
29343
|
+
governingProfileId
|
|
28220
29344
|
});
|
|
28221
29345
|
tryAppendAudit2(
|
|
28222
29346
|
cfg,
|
|
@@ -28225,7 +29349,15 @@ function reconcileProjectPolicy(cfg, credential, projectId) {
|
|
|
28225
29349
|
"policy.reconcile",
|
|
28226
29350
|
"info",
|
|
28227
29351
|
"policy",
|
|
28228
|
-
{
|
|
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
|
+
},
|
|
28229
29361
|
principal.tokenId
|
|
28230
29362
|
)
|
|
28231
29363
|
);
|
|
@@ -28241,8 +29373,19 @@ function getProjectConfig(cfg, credential, projectId) {
|
|
|
28241
29373
|
const root = resolveProjectRoot(cfg.dataDir, principal, projectId);
|
|
28242
29374
|
if (!root) throw new Error(`Unknown project "${projectId}".`);
|
|
28243
29375
|
const projectType = readProjectType(root);
|
|
29376
|
+
const selection = readProjectProfileSelection(root);
|
|
29377
|
+
const classified = classifyProfile(projectType, executeApprovedListProjectProfiles(cfg, projectId));
|
|
28244
29378
|
const locked = runWithProjectRoot(root, () => hostCore.readLockRecord() !== null);
|
|
28245
|
-
|
|
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;
|
|
28246
29389
|
}
|
|
28247
29390
|
function setProjectType(cfg, credential, projectId, projectType) {
|
|
28248
29391
|
const principal = requirePrincipal4(cfg, credential);
|
|
@@ -28253,9 +29396,23 @@ function setProjectType(cfg, credential, projectId, projectType) {
|
|
|
28253
29396
|
}
|
|
28254
29397
|
const root = resolveProjectRoot(cfg.dataDir, principal, projectId);
|
|
28255
29398
|
if (!root) throw new Error(`Unknown project "${projectId}".`);
|
|
28256
|
-
|
|
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);
|
|
28257
29403
|
const locked = runWithProjectRoot(root, () => hostCore.readLockRecord() !== null);
|
|
28258
|
-
|
|
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;
|
|
28259
29416
|
}
|
|
28260
29417
|
function getPackPolicy(cfg, credential) {
|
|
28261
29418
|
requirePrincipal4(cfg, credential);
|
|
@@ -28425,11 +29582,8 @@ function mintToken(cfg, credential, request) {
|
|
|
28425
29582
|
}
|
|
28426
29583
|
assertNotReservedSubjectId(cfg, [request.ownerUserId]);
|
|
28427
29584
|
const projects = request.projects?.length ? request.projects : ["*"];
|
|
28428
|
-
const knownProjects = new Set(listProjectRecords(cfg.dataDir).map((p) => p.id));
|
|
28429
29585
|
for (const p of projects) {
|
|
28430
|
-
|
|
28431
|
-
throw new Error(`unknown project "${p}"`);
|
|
28432
|
-
}
|
|
29586
|
+
assertMintableNarrowingEntry(cfg.dataDir, p);
|
|
28433
29587
|
}
|
|
28434
29588
|
const owner = findUserByRecordOrSubjectId(cfg.dataDir, request.ownerUserId);
|
|
28435
29589
|
if (owner && owner.status !== "active") {
|
|
@@ -28466,12 +29620,21 @@ function revokeToken(cfg, credential, tokenId) {
|
|
|
28466
29620
|
}
|
|
28467
29621
|
function mintSelfToken(cfg, credential, projectId, write) {
|
|
28468
29622
|
const principal = requirePrincipal5(cfg, credential);
|
|
28469
|
-
|
|
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") {
|
|
28470
29630
|
throw new ForbiddenError("caller lacks project:read on the requested project");
|
|
28471
29631
|
}
|
|
28472
|
-
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") {
|
|
28473
29633
|
throw new ForbiddenError("caller lacks project:write on the requested project");
|
|
28474
29634
|
}
|
|
29635
|
+
if (parsed.mounts.length > 0) {
|
|
29636
|
+
assertMintableNarrowingEntry(cfg.dataDir, projectId);
|
|
29637
|
+
}
|
|
28475
29638
|
const token = "wk_" + crypto10.randomBytes(24).toString("hex");
|
|
28476
29639
|
const owner = auditActor(principal);
|
|
28477
29640
|
const record2 = {
|
|
@@ -29083,12 +30246,12 @@ function resolveVisibility(observerProjectId, units, placements) {
|
|
|
29083
30246
|
const best = /* @__PURE__ */ new Map();
|
|
29084
30247
|
for (const placement of placements) {
|
|
29085
30248
|
if (placement.projectId === observerProjectId) continue;
|
|
29086
|
-
const
|
|
29087
|
-
if (!
|
|
29088
|
-
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));
|
|
29089
30252
|
if (!closedOk) continue;
|
|
29090
|
-
const crossTenant = !tenantRoots.has(
|
|
29091
|
-
if (crossTenant && !
|
|
30253
|
+
const crossTenant = !tenantRoots.has(path61[path61.length - 1].id);
|
|
30254
|
+
if (crossTenant && !path61.some(grantedTo)) continue;
|
|
29092
30255
|
if (crossTenant && directUnits.length === 0) continue;
|
|
29093
30256
|
const distance = crossTenant ? "partner" : sameBranch(placement.unitId) ? "department" : "instance";
|
|
29094
30257
|
const existing = best.get(placement.projectId);
|
|
@@ -29111,6 +30274,17 @@ function audienceDistance(resolution, targetProjectId) {
|
|
|
29111
30274
|
|
|
29112
30275
|
// src/server/landscape.ts
|
|
29113
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
|
|
29114
30288
|
var PROJECT_ADMIN_CAPABILITY3 = "project:admin";
|
|
29115
30289
|
var PROJECT_READ_CAPABILITY3 = "project:read";
|
|
29116
30290
|
var PROJECT_WRITE_CAPABILITY3 = "project:write";
|
|
@@ -29597,7 +30771,7 @@ function getProjectSurfaceForMcp(cfg, credential, currentProjectId, targetProjec
|
|
|
29597
30771
|
const result = runWithProjectRoot(record2.rootPath, () => hostSurfaces.exportBoundSurface(maxAudience, "native"));
|
|
29598
30772
|
return { ...result.snapshot, origin: "exchanged" };
|
|
29599
30773
|
}
|
|
29600
|
-
function exportProjectSurface(cfg, credential, projectId, format, maxAudience) {
|
|
30774
|
+
function exportProjectSurface(cfg, credential, projectId, format, maxAudience, portalId) {
|
|
29601
30775
|
const principal = requirePrincipal6(cfg, credential);
|
|
29602
30776
|
if (!permitsCap(cfg, principal, PROJECT_ADMIN_CAPABILITY3, "project", projectId)) {
|
|
29603
30777
|
throw new ForbiddenError(
|
|
@@ -29611,16 +30785,37 @@ function exportProjectSurface(cfg, credential, projectId, format, maxAudience) {
|
|
|
29611
30785
|
if (!root) throw new Error(`Unknown project "${projectId}".`);
|
|
29612
30786
|
const result = runWithProjectRoot(root, () => hostSurfaces.exportBoundSurface(maxAudience, format));
|
|
29613
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
|
+
}
|
|
29614
30809
|
return {
|
|
29615
|
-
body:
|
|
30810
|
+
body: openApiIndexDocument(specs),
|
|
29616
30811
|
contentType: "application/json",
|
|
29617
|
-
filename: `${projectId}-surface.openapi.json`
|
|
30812
|
+
filename: `${safeFilenamePart(projectId)}-surface.openapi.index.json`
|
|
29618
30813
|
};
|
|
29619
30814
|
}
|
|
29620
30815
|
return {
|
|
29621
30816
|
body: yamlLib.dump(result.snapshot),
|
|
29622
30817
|
contentType: "application/yaml",
|
|
29623
|
-
filename: `${projectId}-surface.yaml`
|
|
30818
|
+
filename: `${safeFilenamePart(projectId)}-surface.yaml`
|
|
29624
30819
|
};
|
|
29625
30820
|
}
|
|
29626
30821
|
function removeRelation(cfg, credential, id) {
|
|
@@ -29664,7 +30859,8 @@ function handleLandscapeRequest(cfg, credential, req, res, body, url) {
|
|
|
29664
30859
|
credential,
|
|
29665
30860
|
parts[2],
|
|
29666
30861
|
url.searchParams.get("format") ?? "native",
|
|
29667
|
-
url.searchParams.get("audience") ?? "instance"
|
|
30862
|
+
url.searchParams.get("audience") ?? "instance",
|
|
30863
|
+
url.searchParams.get("spec") ?? void 0
|
|
29668
30864
|
);
|
|
29669
30865
|
res.writeHead(200, {
|
|
29670
30866
|
"content-type": artifact.contentType,
|
|
@@ -29994,10 +31190,9 @@ function migratePermissionModel(dataDir, apply) {
|
|
|
29994
31190
|
// src/server/http.ts
|
|
29995
31191
|
var http2 = __toESM(require("http"));
|
|
29996
31192
|
var fs49 = __toESM(require("fs"));
|
|
29997
|
-
var
|
|
31193
|
+
var path58 = __toESM(require("path"));
|
|
29998
31194
|
|
|
29999
31195
|
// src/server/request.ts
|
|
30000
|
-
var path58 = __toESM(require("path"));
|
|
30001
31196
|
var import_streamableHttp = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
|
|
30002
31197
|
init_fs();
|
|
30003
31198
|
|
|
@@ -30370,38 +31565,58 @@ function initializeProject(cfg, credential, request) {
|
|
|
30370
31565
|
}
|
|
30371
31566
|
}
|
|
30372
31567
|
}
|
|
30373
|
-
function lockProject2(cfg, credential, projectId) {
|
|
31568
|
+
function lockProject2(cfg, credential, projectId, subproject) {
|
|
30374
31569
|
return lifecycleAction(cfg, credential, projectId, {
|
|
30375
31570
|
action: "project:lock",
|
|
30376
31571
|
verb: "Lock",
|
|
30377
31572
|
noun: "lock",
|
|
31573
|
+
subproject,
|
|
30378
31574
|
execute: () => {
|
|
30379
|
-
const lock = executeApprovedLock(cfg, projectId);
|
|
31575
|
+
const lock = executeApprovedLock(cfg, projectId, subproject);
|
|
30380
31576
|
return {
|
|
30381
31577
|
status: "completed",
|
|
30382
31578
|
action: "project:lock",
|
|
30383
|
-
summary: `Locked project "${projectId}" (status: ${lock.status}).`,
|
|
31579
|
+
summary: `Locked project "${projectId}"${subprojectSuffix(subproject)} (status: ${lock.status}).`,
|
|
30384
31580
|
lock
|
|
30385
31581
|
};
|
|
30386
31582
|
}
|
|
30387
31583
|
});
|
|
30388
31584
|
}
|
|
30389
|
-
function promoteProject2(cfg, credential, projectId) {
|
|
31585
|
+
function promoteProject2(cfg, credential, projectId, subproject) {
|
|
30390
31586
|
return lifecycleAction(cfg, credential, projectId, {
|
|
30391
31587
|
action: "project:promote",
|
|
30392
31588
|
verb: "Promote",
|
|
30393
31589
|
noun: "promotion",
|
|
31590
|
+
subproject,
|
|
30394
31591
|
execute: () => {
|
|
30395
|
-
const promo = executeApprovedPromote(cfg, projectId);
|
|
31592
|
+
const promo = executeApprovedPromote(cfg, projectId, subproject);
|
|
30396
31593
|
return {
|
|
30397
31594
|
status: "completed",
|
|
30398
31595
|
action: "project:promote",
|
|
30399
|
-
summary: `Promotion of project "${projectId}": ${promo.status} \u2014 ${promo.message}`,
|
|
31596
|
+
summary: `Promotion of project "${projectId}"${subprojectSuffix(subproject)}: ${promo.status} \u2014 ${promo.message}`,
|
|
30400
31597
|
promote: promo
|
|
30401
31598
|
};
|
|
30402
31599
|
}
|
|
30403
31600
|
});
|
|
30404
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
|
+
}
|
|
30405
31620
|
function lifecycleAction(cfg, credential, projectId, opts) {
|
|
30406
31621
|
const principal = requirePrincipal7(cfg, credential);
|
|
30407
31622
|
const effective = authorize(cfg.dataDir, principal, PROJECT_WRITE_CAPABILITY4, "project", projectId);
|
|
@@ -30427,8 +31642,9 @@ function lifecycleAction(cfg, credential, projectId, opts) {
|
|
|
30427
31642
|
const pending = buildPendingRequest(
|
|
30428
31643
|
principal,
|
|
30429
31644
|
opts.action,
|
|
30430
|
-
`${opts.verb} project ${projectId}`,
|
|
30431
|
-
projectId
|
|
31645
|
+
`${opts.verb} project ${projectId}${subprojectSuffix(opts.subproject)}`,
|
|
31646
|
+
projectId,
|
|
31647
|
+
subprojectScopePayload(opts.subproject)
|
|
30432
31648
|
);
|
|
30433
31649
|
return createPendingOutcome(cfg, principal, pending, opts.action);
|
|
30434
31650
|
}
|
|
@@ -30502,12 +31718,14 @@ function executeApproved(cfg, req) {
|
|
|
30502
31718
|
return `Initialized project "${rec.id}" under the active pack policy.`;
|
|
30503
31719
|
}
|
|
30504
31720
|
case "project:lock": {
|
|
30505
|
-
const
|
|
30506
|
-
|
|
31721
|
+
const scope = readSubprojectScope(req);
|
|
31722
|
+
const lock = executeApprovedLock(cfg, req.projectId ?? "", scope);
|
|
31723
|
+
return `Locked project "${req.projectId}"${subprojectSuffix(scope)} (status: ${lock.status}).`;
|
|
30507
31724
|
}
|
|
30508
31725
|
case "project:promote": {
|
|
30509
|
-
const
|
|
30510
|
-
|
|
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}`;
|
|
30511
31729
|
}
|
|
30512
31730
|
default:
|
|
30513
31731
|
throw new Error(`Unsupported approval kind "${req.kind}".`);
|
|
@@ -31211,6 +32429,9 @@ function getProjectConfig2(cfg, credential, projectId) {
|
|
|
31211
32429
|
function setProjectType2(cfg, credential, projectId, projectType) {
|
|
31212
32430
|
return setProjectType(cfg, credential, projectId, projectType);
|
|
31213
32431
|
}
|
|
32432
|
+
function listProjectProfiles2(cfg, credential, project2) {
|
|
32433
|
+
return listProjectProfiles(cfg, credential, project2);
|
|
32434
|
+
}
|
|
31214
32435
|
function listProducers2(cfg, credential, project2) {
|
|
31215
32436
|
return listProducers(cfg, credential, project2);
|
|
31216
32437
|
}
|
|
@@ -31739,6 +32960,19 @@ var ShareSnapshotRegistry = class {
|
|
|
31739
32960
|
return stored;
|
|
31740
32961
|
}
|
|
31741
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
|
+
}
|
|
31742
32976
|
var ShareSnapshotIndex = class {
|
|
31743
32977
|
constructor(store) {
|
|
31744
32978
|
this.store = store;
|
|
@@ -31746,12 +32980,12 @@ var ShareSnapshotIndex = class {
|
|
|
31746
32980
|
get(snapshotId) {
|
|
31747
32981
|
return this.store.read(snapshotId);
|
|
31748
32982
|
}
|
|
31749
|
-
getArtifact(snapshotId, kind) {
|
|
32983
|
+
getArtifact(snapshotId, kind, portalId) {
|
|
31750
32984
|
const snap = this.store.read(snapshotId);
|
|
31751
32985
|
if (!snap) return null;
|
|
31752
32986
|
if (kind === "canvas") return snap.canvasModel ?? null;
|
|
31753
32987
|
if (kind === "html") return snap.html ?? null;
|
|
31754
|
-
if (kind === "openapi") return snap
|
|
32988
|
+
if (kind === "openapi") return selectCapturedOpenApi(snap, portalId);
|
|
31755
32989
|
return null;
|
|
31756
32990
|
}
|
|
31757
32991
|
};
|
|
@@ -31761,8 +32995,8 @@ function putSnapshot(dataDir, snapshot) {
|
|
|
31761
32995
|
function getSnapshot2(dataDir, snapshotId) {
|
|
31762
32996
|
return new ShareSnapshotIndex(new ShareSnapshotStore(dataDir)).get(snapshotId);
|
|
31763
32997
|
}
|
|
31764
|
-
function getSnapshotArtifact(dataDir, snapshotId, kind) {
|
|
31765
|
-
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);
|
|
31766
33000
|
}
|
|
31767
33001
|
function captureSnapshot(dataDir, principal, projectId, view, artifacts) {
|
|
31768
33002
|
const root = resolveProjectRoot(dataDir, principal, projectId);
|
|
@@ -31781,7 +33015,11 @@ function captureSnapshot(dataDir, principal, projectId, view, artifacts) {
|
|
|
31781
33015
|
}
|
|
31782
33016
|
if (artifacts.includes("openapi")) {
|
|
31783
33017
|
const result = hostSurfaces.exportBoundSurface("project", "openapi");
|
|
31784
|
-
|
|
33018
|
+
const specs = result.renderedSet ?? [];
|
|
33019
|
+
if (specs.length) {
|
|
33020
|
+
snapshot.openapiSet = specs;
|
|
33021
|
+
if (specs.length === 1) snapshot.openapi = specs[0].document;
|
|
33022
|
+
}
|
|
31785
33023
|
}
|
|
31786
33024
|
return snapshot;
|
|
31787
33025
|
});
|
|
@@ -32342,10 +33580,10 @@ function getWebProjectOpenApi(cfg, sessionId, projectId, portalId) {
|
|
|
32342
33580
|
if (!root) throw new ForbiddenError("project not authorized or unknown");
|
|
32343
33581
|
const result = runWithProjectRoot(root, () => hostSurfaces.exportBoundSurface("project", "openapi"));
|
|
32344
33582
|
const specs = result.renderedSet ?? (result.rendered ? [{ portalId: "", name: projectId, document: result.rendered }] : []);
|
|
32345
|
-
if (specs.length === 0) return
|
|
33583
|
+
if (specs.length === 0) return openApiIndexPage(projectId, []);
|
|
32346
33584
|
if (portalId) {
|
|
32347
|
-
const sel = specs.find((s) => s.portalId === portalId)
|
|
32348
|
-
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);
|
|
32349
33587
|
}
|
|
32350
33588
|
if (specs.length === 1) return swaggerUiPage(specs[0].document, specs[0].name);
|
|
32351
33589
|
return openApiIndexPage(projectId, specs);
|
|
@@ -32353,7 +33591,7 @@ function getWebProjectOpenApi(cfg, sessionId, projectId, portalId) {
|
|
|
32353
33591
|
function openApiIndexPage(projectId, specs) {
|
|
32354
33592
|
const esc2 = (s) => s.replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c]);
|
|
32355
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("");
|
|
32356
|
-
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>`;
|
|
32357
33595
|
}
|
|
32358
33596
|
function reshapeLandscapeGraph(model, level) {
|
|
32359
33597
|
const unitNodeIds = new Set(model.nodes.filter((n) => n.nodeKind === "orgUnit").map((n) => n.id));
|
|
@@ -32387,7 +33625,31 @@ function reshapeLandscapeGraph(model, level) {
|
|
|
32387
33625
|
}
|
|
32388
33626
|
const kept = nodes.filter((n) => n.level <= level);
|
|
32389
33627
|
const keptIds = new Set(kept.map((n) => n.id));
|
|
32390
|
-
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
|
+
}
|
|
32391
33653
|
return {
|
|
32392
33654
|
tier: "landscape",
|
|
32393
33655
|
nodes: kept,
|
|
@@ -34531,6 +35793,9 @@ function opsGetProjectConfig(cfg, sessionId, url, res) {
|
|
|
34531
35793
|
function opsSetProjectConfig(cfg, sessionId, body, res) {
|
|
34532
35794
|
sendJson(res, 200, setProjectType2(cfg, sessionId, String(body?.projectId ?? ""), String(body?.projectType ?? "")));
|
|
34533
35795
|
}
|
|
35796
|
+
function opsListProjectProfiles(cfg, sessionId, url, res) {
|
|
35797
|
+
sendJson(res, 200, { profiles: listProjectProfiles2(cfg, sessionId, q(url, "projectId") ?? "") });
|
|
35798
|
+
}
|
|
34534
35799
|
function opsListProducers(cfg, sessionId, url, res) {
|
|
34535
35800
|
sendJson(res, 200, { producers: listProducers2(cfg, sessionId, q(url, "projectId") ?? "") });
|
|
34536
35801
|
}
|
|
@@ -34770,6 +36035,9 @@ async function handleWebRequest(cfg, req, res, body, url, ctx) {
|
|
|
34770
36035
|
if (req.method === "POST" && parts.length === 3 && parts[2] === "config") {
|
|
34771
36036
|
return opsSetProjectConfig(cfg, sessionId, body, res);
|
|
34772
36037
|
}
|
|
36038
|
+
if (req.method === "GET" && parts.length === 3 && parts[2] === "profiles") {
|
|
36039
|
+
return opsListProjectProfiles(cfg, sessionId, url, res);
|
|
36040
|
+
}
|
|
34773
36041
|
if (req.method === "GET" && parts.length === 3 && parts[2] === "policy") {
|
|
34774
36042
|
return opsPolicyEvaluate(cfg, sessionId, url, res);
|
|
34775
36043
|
}
|
|
@@ -34994,8 +36262,8 @@ var RealtimeHub = class {
|
|
|
34994
36262
|
* complete the handshake, and register the connection. A bad path or session
|
|
34995
36263
|
* destroys the socket. */
|
|
34996
36264
|
handleUpgrade(cfg, req, socket) {
|
|
34997
|
-
const
|
|
34998
|
-
if (
|
|
36265
|
+
const path61 = (req.url ?? "/").split("?")[0];
|
|
36266
|
+
if (path61 !== REALTIME_PATH || !isWebSocketUpgrade(req)) {
|
|
34999
36267
|
socket.destroy();
|
|
35000
36268
|
return;
|
|
35001
36269
|
}
|
|
@@ -35140,7 +36408,7 @@ function deriveMcpOutcome(response) {
|
|
|
35140
36408
|
if (r.result && typeof r.result === "object" && r.result.isError) return "failed";
|
|
35141
36409
|
return "success";
|
|
35142
36410
|
}
|
|
35143
|
-
function auditToolCall(dataDir, principal, projectId, body, outcome) {
|
|
36411
|
+
function auditToolCall(dataDir, principal, projectId, body, outcome, subproject) {
|
|
35144
36412
|
const target = mcpToolTarget(body);
|
|
35145
36413
|
if (!target) return;
|
|
35146
36414
|
const actor = principal.subject ?? {
|
|
@@ -35158,7 +36426,8 @@ function auditToolCall(dataDir, principal, projectId, body, outcome) {
|
|
|
35158
36426
|
actor,
|
|
35159
36427
|
tokenId: principal.tokenId,
|
|
35160
36428
|
projectId,
|
|
35161
|
-
target
|
|
36429
|
+
target,
|
|
36430
|
+
...subproject ? { metadata: JSON.stringify({ subproject }) } : {}
|
|
35162
36431
|
};
|
|
35163
36432
|
try {
|
|
35164
36433
|
appendAuditEvent(dataDir, event, DEFAULT_AUDIT_POLICY);
|
|
@@ -35189,6 +36458,12 @@ var PROJECT_OPS_TOOLS = /* @__PURE__ */ new Set([
|
|
|
35189
36458
|
"sdd_host_produce",
|
|
35190
36459
|
"sdd_host_commit_project"
|
|
35191
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
|
+
]);
|
|
35192
36467
|
function jsonRpcRequests(body) {
|
|
35193
36468
|
const arr = Array.isArray(body) ? body : [body];
|
|
35194
36469
|
return arr.filter((m) => !!m && typeof m === "object" && "method" in m);
|
|
@@ -35254,7 +36529,27 @@ function dataPlanePermissionError(cfg, principal, projectId, body) {
|
|
|
35254
36529
|
}
|
|
35255
36530
|
};
|
|
35256
36531
|
}
|
|
35257
|
-
|
|
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) {
|
|
35258
36553
|
const msg = jsonRpcRequest(body);
|
|
35259
36554
|
if (!msg || msg.method !== "tools/call") return void 0;
|
|
35260
36555
|
const name = msg.params?.name;
|
|
@@ -35271,10 +36566,10 @@ async function dispatchProjectLifecycleTool(cfg, credential, projectId, body) {
|
|
|
35271
36566
|
value = initializeProject(cfg, credential, args);
|
|
35272
36567
|
break;
|
|
35273
36568
|
case "sdd_host_lock_project":
|
|
35274
|
-
value = lockProject2(cfg, credential, projectId);
|
|
36569
|
+
value = lockProject2(cfg, credential, projectId, subproject);
|
|
35275
36570
|
break;
|
|
35276
36571
|
case "sdd_host_promote_project":
|
|
35277
|
-
value = promoteProject2(cfg, credential, projectId);
|
|
36572
|
+
value = promoteProject2(cfg, credential, projectId, subproject);
|
|
35278
36573
|
break;
|
|
35279
36574
|
case "sdd_host_await_approval":
|
|
35280
36575
|
value = await awaitApproval(
|
|
@@ -35362,8 +36657,8 @@ async function handleMcpRequest(cfg, req, res, body, credential) {
|
|
|
35362
36657
|
permissionSubject: { subjectId: "anonymous", roleBindings: [], instanceAdmin: true }
|
|
35363
36658
|
};
|
|
35364
36659
|
}
|
|
35365
|
-
const
|
|
35366
|
-
if (!
|
|
36660
|
+
const binding = resolveProjectBinding(cfg.dataDir, principal, projectSelector(req));
|
|
36661
|
+
if (!binding) {
|
|
35367
36662
|
sendJson(res, 403, { error: "project not authorized, unknown, or not specified" });
|
|
35368
36663
|
return;
|
|
35369
36664
|
}
|
|
@@ -35378,19 +36673,26 @@ async function handleMcpRequest(cfg, req, res, body, credential) {
|
|
|
35378
36673
|
});
|
|
35379
36674
|
return;
|
|
35380
36675
|
}
|
|
35381
|
-
await runWithProjectRoot(
|
|
35382
|
-
const projectId =
|
|
35383
|
-
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);
|
|
35384
36686
|
if (dispatchedResponse !== void 0) {
|
|
35385
36687
|
sendJson(res, 200, dispatchedResponse);
|
|
35386
|
-
auditToolCall(cfg.dataDir, principal, projectId, body, deriveMcpOutcome(dispatchedResponse));
|
|
36688
|
+
auditToolCall(cfg.dataDir, principal, projectId, body, deriveMcpOutcome(dispatchedResponse), subproject);
|
|
35387
36689
|
for (const ch of mcpChangeChannels(body, projectId, dispatchedResponse)) publishChange(ch);
|
|
35388
36690
|
return;
|
|
35389
36691
|
}
|
|
35390
36692
|
const permissionError = dataPlanePermissionError(cfg, principal, projectId, body);
|
|
35391
36693
|
if (permissionError !== void 0) {
|
|
35392
36694
|
sendJson(res, 200, permissionError);
|
|
35393
|
-
auditToolCall(cfg.dataDir, principal, projectId, body, deriveMcpOutcome(permissionError));
|
|
36695
|
+
auditToolCall(cfg.dataDir, principal, projectId, body, deriveMcpOutcome(permissionError), subproject);
|
|
35394
36696
|
return;
|
|
35395
36697
|
}
|
|
35396
36698
|
const server = createScopedServer();
|
|
@@ -35411,7 +36713,7 @@ async function handleMcpRequest(cfg, req, res, body, credential) {
|
|
|
35411
36713
|
};
|
|
35412
36714
|
await server.connect(transport);
|
|
35413
36715
|
await transport.handleRequest(req, res, body);
|
|
35414
|
-
auditToolCall(cfg.dataDir, principal, projectId, body, deriveMcpOutcome(response));
|
|
36716
|
+
auditToolCall(cfg.dataDir, principal, projectId, body, deriveMcpOutcome(response), subproject);
|
|
35415
36717
|
for (const ch of mcpChangeChannels(body, projectId, response)) publishChange(ch);
|
|
35416
36718
|
});
|
|
35417
36719
|
}
|
|
@@ -35479,7 +36781,7 @@ var CONTENT_TYPE = {
|
|
|
35479
36781
|
openapi: "application/json",
|
|
35480
36782
|
canvas: "application/json"
|
|
35481
36783
|
};
|
|
35482
|
-
function downloadArtifact(cfg, token, kind, meta) {
|
|
36784
|
+
function downloadArtifact(cfg, token, kind, meta, portalId) {
|
|
35483
36785
|
const link = linkByTokenHash(cfg.dataDir, hashToken(token));
|
|
35484
36786
|
const check = usable(link);
|
|
35485
36787
|
if ("outcome" in check) {
|
|
@@ -35491,7 +36793,7 @@ function downloadArtifact(cfg, token, kind, meta) {
|
|
|
35491
36793
|
record(cfg.dataDir, check.link.id, meta, "denied-download");
|
|
35492
36794
|
return { found: false, outcome: "denied-download" };
|
|
35493
36795
|
}
|
|
35494
|
-
const content = getSnapshotArtifact(cfg.dataDir, check.link.snapshotId, kind);
|
|
36796
|
+
const content = getSnapshotArtifact(cfg.dataDir, check.link.snapshotId, kind, portalId);
|
|
35495
36797
|
if (content === null) {
|
|
35496
36798
|
record(cfg.dataDir, check.link.id, meta, "not-found");
|
|
35497
36799
|
return { found: false, outcome: "not-found" };
|
|
@@ -35501,6 +36803,35 @@ function downloadArtifact(cfg, token, kind, meta) {
|
|
|
35501
36803
|
}
|
|
35502
36804
|
|
|
35503
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
|
+
}
|
|
35504
36835
|
function shareRequestMeta(req) {
|
|
35505
36836
|
const fwd = req.headers["x-forwarded-for"];
|
|
35506
36837
|
const ip = (Array.isArray(fwd) ? fwd[0] : fwd)?.split(",")[0].trim() || req.socket?.remoteAddress || "unknown";
|
|
@@ -35552,14 +36883,16 @@ function serveSharedModel(cfg, token, req, res) {
|
|
|
35552
36883
|
);
|
|
35553
36884
|
}
|
|
35554
36885
|
function serveSharedOpenApi(cfg, token, req, res) {
|
|
35555
|
-
const result = downloadArtifact(cfg, token, "openapi", shareRequestMeta(req));
|
|
36886
|
+
const result = downloadArtifact(cfg, token, "openapi", shareRequestMeta(req), specParam(req));
|
|
35556
36887
|
if (!result.found || result.content === void 0) return notFoundPage(res);
|
|
36888
|
+
const index = asOpenApiIndex(result.content);
|
|
35557
36889
|
harden(res, void 0, "text/html; charset=utf-8");
|
|
35558
36890
|
res.statusCode = 200;
|
|
35559
|
-
res.end(swaggerUiPage(result.content, "Shared API"));
|
|
36891
|
+
res.end(index ? sharedOpenApiIndexPage(index) : swaggerUiPage(result.content, "Shared API"));
|
|
35560
36892
|
}
|
|
35561
36893
|
function serveSharedDownload(cfg, token, kind, req, res) {
|
|
35562
|
-
const
|
|
36894
|
+
const portalId = specParam(req);
|
|
36895
|
+
const result = downloadArtifact(cfg, token, kind, shareRequestMeta(req), portalId);
|
|
35563
36896
|
if (!result.found || result.content === void 0) {
|
|
35564
36897
|
if (result.outcome === "denied-download") {
|
|
35565
36898
|
harden(res, void 0, "text/plain");
|
|
@@ -35569,9 +36902,11 @@ function serveSharedDownload(cfg, token, kind, req, res) {
|
|
|
35569
36902
|
}
|
|
35570
36903
|
return notFoundPage(res);
|
|
35571
36904
|
}
|
|
35572
|
-
const ext = kind === "html" ? "html" : kind === "openapi" ? "openapi.json" : kind;
|
|
35573
36905
|
harden(res, void 0, result.contentType ?? "application/octet-stream");
|
|
35574
|
-
res.setHeader(
|
|
36906
|
+
res.setHeader(
|
|
36907
|
+
"content-disposition",
|
|
36908
|
+
`attachment; filename="${downloadFilename(kind, portalId, result.content)}"`
|
|
36909
|
+
);
|
|
35575
36910
|
res.statusCode = 200;
|
|
35576
36911
|
res.end(result.content);
|
|
35577
36912
|
}
|
|
@@ -35813,7 +37148,7 @@ function routeData(cfg, req, res) {
|
|
|
35813
37148
|
}
|
|
35814
37149
|
function readExposurePolicyFile(dataDir) {
|
|
35815
37150
|
try {
|
|
35816
|
-
const raw = fs49.readFileSync(
|
|
37151
|
+
const raw = fs49.readFileSync(path58.join(dataDir, "exposure-policy.json"), "utf8");
|
|
35817
37152
|
const parsed = JSON.parse(raw);
|
|
35818
37153
|
return parsed && typeof parsed === "object" ? parsed : void 0;
|
|
35819
37154
|
} catch {
|
|
@@ -36498,9 +37833,9 @@ function seedDemoTree() {
|
|
|
36498
37833
|
|
|
36499
37834
|
// src/commands/host.ts
|
|
36500
37835
|
function resolveHostConfig(options) {
|
|
36501
|
-
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");
|
|
36502
37837
|
if (!process.env["WAIRON_PACKS_DIR"]) {
|
|
36503
|
-
process.env["WAIRON_PACKS_DIR"] =
|
|
37838
|
+
process.env["WAIRON_PACKS_DIR"] = path59.join(dataDir, "packs");
|
|
36504
37839
|
}
|
|
36505
37840
|
const cfg = {
|
|
36506
37841
|
host: options.host || "0.0.0.0",
|
|
@@ -36625,13 +37960,13 @@ function openBrowser(url) {
|
|
|
36625
37960
|
}
|
|
36626
37961
|
async function runDev(options = {}) {
|
|
36627
37962
|
const cwd = process.cwd();
|
|
36628
|
-
if (!fs50.existsSync(
|
|
37963
|
+
if (!fs50.existsSync(path59.join(cwd, ".wai"))) {
|
|
36629
37964
|
throw new WaironError(
|
|
36630
37965
|
"No .wai/ found in the current directory. Run `wairon dev` from a wairon project root (or run `wairon init` first)."
|
|
36631
37966
|
);
|
|
36632
37967
|
}
|
|
36633
37968
|
const hash = crypto20.createHash("sha256").update(cwd).digest("hex").slice(0, 16);
|
|
36634
|
-
const dataDir =
|
|
37969
|
+
const dataDir = path59.join(os10.tmpdir(), "wairon-dev", hash);
|
|
36635
37970
|
fs50.mkdirSync(dataDir, { recursive: true });
|
|
36636
37971
|
registerLocalDevProject(dataDir, "local", cwd);
|
|
36637
37972
|
const port = options.port ? Number(options.port) : 8080;
|
|
@@ -37040,8 +38375,8 @@ async function runHostPacks(action, options = {}) {
|
|
|
37040
38375
|
}
|
|
37041
38376
|
case "install": {
|
|
37042
38377
|
if (!options.file) throw new WaironError("`--file <path>` (a declarative pack YAML) is required for install.");
|
|
37043
|
-
const name = options.name ??
|
|
37044
|
-
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");
|
|
37045
38380
|
const desc = project2 ? installProjectPack(cfg, cred, project2, name, content) : installGlobalPack(cfg, cred, name, content);
|
|
37046
38381
|
logger.success(`Installed ${scope} pack "${desc.name}" (${desc.profiles} profile(s), ${desc.languages} language(s)).`);
|
|
37047
38382
|
if (project2) logger.info("Committed with the project \u2014 every clone and CI will enforce it.");
|
|
@@ -37181,15 +38516,29 @@ async function runSurface(action, options = {}) {
|
|
|
37181
38516
|
if (format !== "native" && format !== "openapi") {
|
|
37182
38517
|
throw new WaironError(`Unknown format "${format}" (supported: native, openapi).`);
|
|
37183
38518
|
}
|
|
37184
|
-
|
|
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);
|
|
37185
38523
|
logger.success(
|
|
37186
38524
|
`Projected surface of "${result.snapshot.projectName}": ${result.snapshot.interfaces.length} interface(s), ${result.snapshot.types.length} type(s) at audience \u2265 ${audience}.`
|
|
37187
38525
|
);
|
|
37188
|
-
|
|
37189
|
-
|
|
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}`);
|
|
37190
38532
|
} else if (result.rendered) {
|
|
37191
38533
|
process.stdout.write(`${result.rendered}
|
|
37192
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
|
+
}
|
|
37193
38542
|
} else {
|
|
37194
38543
|
for (const entry of result.snapshot.interfaces) {
|
|
37195
38544
|
logger.info(` ${import_chalk19.default.cyan(entry.id)} (${entry.type}, ${entry.audience}) \u2014 ${entry.methods.length} method(s)`);
|
|
@@ -37233,13 +38582,27 @@ async function runSurface(action, options = {}) {
|
|
|
37233
38582
|
for (const p of written) logger.info(` ${p}`);
|
|
37234
38583
|
return;
|
|
37235
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
|
+
}
|
|
37236
38599
|
default:
|
|
37237
|
-
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).`);
|
|
37238
38601
|
}
|
|
37239
38602
|
}
|
|
37240
38603
|
|
|
37241
38604
|
// src/commands/subsystem.ts
|
|
37242
|
-
var
|
|
38605
|
+
var path60 = __toESM(require("path"));
|
|
37243
38606
|
init_logger();
|
|
37244
38607
|
init_errors();
|
|
37245
38608
|
init_fs();
|
|
@@ -37275,9 +38638,9 @@ async function runSubsystemAdd(id, options = {}) {
|
|
|
37275
38638
|
updatedAt: now
|
|
37276
38639
|
};
|
|
37277
38640
|
createChainedSubsystem(subsystem, displayName);
|
|
37278
|
-
const childDir =
|
|
38641
|
+
const childDir = path60.resolve(getProjectRoot(), options.projectPath);
|
|
37279
38642
|
logger.success(`Added external subsystem "${id}" \u2192 ${options.projectPath}`);
|
|
37280
|
-
logger.info(`Scaffolded child project at ${
|
|
38643
|
+
logger.info(`Scaffolded child project at ${path60.relative(process.cwd(), childDir) || "."}`);
|
|
37281
38644
|
logger.info(`Design its spec tree from this parent using namespaced ids (e.g. ${id}::<component>).`);
|
|
37282
38645
|
}
|
|
37283
38646
|
async function runSubsystemMove(id, options = {}) {
|
|
@@ -37300,9 +38663,9 @@ async function runSubsystemExternalize(id, options = {}) {
|
|
|
37300
38663
|
throw new WaironError("--project-path (the subproject destination) is required.");
|
|
37301
38664
|
}
|
|
37302
38665
|
externalizeSubsystem(id, options.projectPath);
|
|
37303
|
-
const childDir =
|
|
38666
|
+
const childDir = path60.resolve(getProjectRoot(), options.projectPath);
|
|
37304
38667
|
logger.success(`Externalized subsystem "${id}" \u2192 ${options.projectPath}`);
|
|
37305
|
-
logger.info(`Moved its specs into ${
|
|
38668
|
+
logger.info(`Moved its specs into ${path60.relative(process.cwd(), childDir) || "."} (now a standalone subproject).`);
|
|
37306
38669
|
logger.info("Move the source code there yourself, then run `wairon validate` to confirm the tree.");
|
|
37307
38670
|
}
|
|
37308
38671
|
async function runSubsystemInternalize(id) {
|
|
@@ -37341,8 +38704,64 @@ program.command("generate").description("Generate agent output files from the sp
|
|
|
37341
38704
|
dryRun: opts.dryRun
|
|
37342
38705
|
});
|
|
37343
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
|
+
}
|
|
37344
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) => {
|
|
37345
|
-
await
|
|
38764
|
+
await runLock2({ yes: opts.yes, subsystem: opts.subsystem, recursive: opts.recursive });
|
|
37346
38765
|
});
|
|
37347
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) => {
|
|
37348
38767
|
await runValidate({ ci: opts.ci, subsystem: opts.subsystem, recursive: opts.recursive });
|
|
@@ -37458,13 +38877,14 @@ mcpCmd.command("status").description("Show whether the wairon MCP server is regi
|
|
|
37458
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) => {
|
|
37459
38878
|
await runProduce(target, { page: opts.page, token: opts.token });
|
|
37460
38879
|
});
|
|
37461
|
-
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) => {
|
|
37462
38881
|
await runSurface(action, {
|
|
37463
38882
|
audience: opts.audience,
|
|
37464
38883
|
format: opts.format,
|
|
37465
38884
|
out: opts.out,
|
|
37466
38885
|
source: opts.source,
|
|
37467
|
-
origin: opts.origin
|
|
38886
|
+
origin: opts.origin,
|
|
38887
|
+
portal: opts.portal
|
|
37468
38888
|
});
|
|
37469
38889
|
});
|
|
37470
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) => {
|