@wairon/cli 5.0.2-dev.4 → 5.0.2-dev.6

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 CHANGED
@@ -65,7 +65,7 @@ var init_defaults = __esm({
65
65
  copilot: ".github/prompts",
66
66
  codex: ".codex/agents"
67
67
  };
68
- WAIRON_VERSION = "5.0.2-dev.4";
68
+ WAIRON_VERSION = "5.0.2-dev.6";
69
69
  GITHUB_REPO = "SYW-Apps/Waffle-AIron";
70
70
  ARCHITECT_AGENT_ID = "agent-architect";
71
71
  ARCHITECT_TEMPLATE_ID = "architect";
@@ -3304,8 +3304,17 @@ var MODEL = __MODEL_JSON__;
3304
3304
  var configuredLineStyle = ['bezier', 'straight', 'taxi'].indexOf(diagramConfig.lineStyle) >= 0 ? diagramConfig.lineStyle : 'bezier';
3305
3305
  var defaultViewKind = diagramConfig.defaultView === 'types' ? 'types' : (diagramConfig.defaultView === 'databases' && showDatabaseTab ? 'databases' : 'system');
3306
3306
 
3307
+ // Stage J: a deep link (opts.initialRoute) seeds the initial view directly so a
3308
+ // refresh / shared URL renders the right scope with NO root-first flash. Parsed
3309
+ // by the same resolver as openRoute (hoisted below). No onViewChange fires for
3310
+ // this initial seed.
3311
+ var initialView = { kind: defaultViewKind, id: null };
3312
+ if (typeof opts !== 'undefined' && opts && typeof opts.initialRoute === 'string' && opts.initialRoute.length) {
3313
+ initialView = resolveRoute(opts.initialRoute);
3314
+ }
3315
+
3307
3316
  var state = {
3308
- view: { kind: defaultViewKind, id: null },
3317
+ view: initialView,
3309
3318
  internals: typeof saved.internals === 'boolean' ? saved.internals : false,
3310
3319
  externals: typeof saved.externals === 'boolean' ? saved.externals : true,
3311
3320
  dataCoupling: typeof saved.dataCoupling === 'boolean' ? saved.dataCoupling : false,
@@ -3324,6 +3333,9 @@ var MODEL = __MODEL_JSON__;
3324
3333
  // Set by buildTypeElements when the ERD is degraded for performance (huge
3325
3334
  // scopes); consumed by renderTypesNotice to explain the level-of-detail.
3326
3335
  var typesNotice = '';
3336
+ // Stage J: true while openRoute is applying a URL-driven view change, so the
3337
+ // onViewChange callback is suppressed and we do not loop URL -> engine -> URL.
3338
+ var applyingRoute = false;
3327
3339
 
3328
3340
  function viewKey() {
3329
3341
  // 'types2' + detail level: table sizes differ per detail, and the prefix
@@ -4753,6 +4765,78 @@ var MODEL = __MODEL_JSON__;
4753
4765
  state.typesRenderAll = false; // a fresh scope re-evaluates the LOD budget
4754
4766
  rebuild(true);
4755
4767
  renderPanel();
4768
+ notifyViewChange();
4769
+ }
4770
+
4771
+ // ---- Stage J: URL <-> view routing ----------------------------------------
4772
+ // The canvas navigation lives in the URL path (refresh-safe + shareable). A
4773
+ // route is the string AFTER /canvas/<project>: '' = system root; a '/'-joined
4774
+ // NAMESPACE (e.g. 'a/b') that resolves against the model to a subsystem or a
4775
+ // component; or the 'types'/'databases' view modes with an optional scope.
4776
+ // A segment is the '::' namespace with '/' as separator, each part encoded.
4777
+ function encSeg(s) { return encodeURIComponent(String(s)); }
4778
+ function routeOf(view) {
4779
+ if (!view) return '';
4780
+ var k = view.kind;
4781
+ if (k === 'system') return '';
4782
+ if (k === 'types' || k === 'databases') {
4783
+ return view.id ? k + '/' + view.id.split('::').map(encSeg).join('/') : k;
4784
+ }
4785
+ if (k === 'subsystem') {
4786
+ return view.id ? view.id.split('::').map(encSeg).join('/') : '';
4787
+ }
4788
+ if (k === 'component') {
4789
+ // A component's full namespace path IS its id: a chained subproject carries
4790
+ // the subsystem prefix in the id (a::b::comp), a flat project uses the bare
4791
+ // id (comp). Serializing comp.id split on '::' round-trips exactly via the
4792
+ // compById lookup in resolveRoute. Owner-pattern nesting is deliberately NOT
4793
+ // encoded (ownership is a separate axis; comp.id does not embed the owner).
4794
+ var c = compById[view.id];
4795
+ var full = c ? c.id : view.id;
4796
+ return full.split('::').map(encSeg).join('/');
4797
+ }
4798
+ return '';
4799
+ }
4800
+ function resolveRoute(routeStr) {
4801
+ var parts = String(routeStr || '').split('/').filter(function (s) { return s.length > 0; }).map(decodeURIComponent);
4802
+ if (!parts.length) return { kind: 'system', id: null };
4803
+ // NOTE: a subsystem literally named 'types'/'databases' is SHADOWED by these
4804
+ // view-mode routes (acceptable \u2014 the modes own those first segments).
4805
+ if (parts[0] === 'types') {
4806
+ return { kind: 'types', id: parts.length > 1 ? parts.slice(1).join('::') : null };
4807
+ }
4808
+ if (parts[0] === 'databases') {
4809
+ if (!showDatabaseTab) return { kind: 'system', id: null };
4810
+ return { kind: 'databases', id: parts.length > 1 ? parts.slice(1).join('::') : null };
4811
+ }
4812
+ var joined = parts.join('::');
4813
+ if (subById[joined]) return { kind: 'subsystem', id: joined };
4814
+ if (compById[joined]) return { kind: 'component', id: joined };
4815
+ return { kind: 'system', id: null }; // unknown id -> fall back to the root
4816
+ }
4817
+ // Apply a route (URL -> engine) WITHOUT echoing back through onViewChange.
4818
+ function openRoute(routeStr) {
4819
+ var v = resolveRoute(routeStr);
4820
+ if (state.view.kind === v.kind && state.view.id === v.id) return;
4821
+ applyingRoute = true;
4822
+ try {
4823
+ state.view = { kind: v.kind, id: v.id };
4824
+ state.selected = null;
4825
+ state.selectedKind = null;
4826
+ state.typesRenderAll = false;
4827
+ rebuild(true);
4828
+ renderPanel();
4829
+ } finally {
4830
+ applyingRoute = false;
4831
+ }
4832
+ }
4833
+ // Notify the embedder (engine -> URL) of the current view; suppressed while a
4834
+ // route is being applied so the URL is not driven in a loop.
4835
+ function notifyViewChange() {
4836
+ if (applyingRoute) return;
4837
+ if (typeof opts !== 'undefined' && opts && typeof opts.onViewChange === 'function') {
4838
+ try { opts.onViewChange(routeOf(state.view)); } catch (e) { /* ignore */ }
4839
+ }
4756
4840
  }
4757
4841
  // Explain (and offer to override) a performance-degraded ERD.
4758
4842
  function renderTypesNotice() {
@@ -4903,6 +4987,7 @@ var MODEL = __MODEL_JSON__;
4903
4987
  if (t.ghost) {
4904
4988
  state.view = parentViewOf(t.kind, t.id);
4905
4989
  rebuild(true);
4990
+ notifyViewChange();
4906
4991
  select(t.kind, t.id, true);
4907
4992
  return;
4908
4993
  }
@@ -5923,10 +6008,12 @@ var MODEL = __MODEL_JSON__;
5923
6008
  if (kind === 'type' && state.view.kind !== 'types' && state.view.kind !== 'databases') {
5924
6009
  state.view = { kind: 'types', id: typesScopeFromView() };
5925
6010
  rebuild(true);
6011
+ notifyViewChange();
5926
6012
  } else if (kind === 'component' && (state.view.kind === 'types' || state.view.kind === 'databases')) {
5927
6013
  var c = compById[id];
5928
6014
  state.view = { kind: 'subsystem', id: c ? c.subsystem : null };
5929
6015
  rebuild(true);
6016
+ notifyViewChange();
5930
6017
  }
5931
6018
  state.selectedKind = kind;
5932
6019
  state.selected = id;
@@ -5985,6 +6072,14 @@ var MODEL = __MODEL_JSON__;
5985
6072
  if (!exposes || !openApiAllowed()) return '';
5986
6073
  return '<div class="openbtn"><button class="tbtn" data-openapi-tag="' + esc(tag || '') + '">\\u25A4 View OpenAPI \\u2197</button></div>';
5987
6074
  }
6075
+ // "Open in Specs" \u2014 deep-links the focused spec into the hosted Specs value
6076
+ // editor via a host hook (opts.onOpenSpec). Hidden when no host provides it
6077
+ // (standalone file, shared page): those have no editor to open. kind is the
6078
+ // spec layer, id its qualified spec id.
6079
+ function openSpecButton(kind, id) {
6080
+ if (typeof opts === 'undefined' || !opts || typeof opts.onOpenSpec !== 'function') return '';
6081
+ return '<div class="openbtn"><button class="tbtn" data-openspec-kind="' + kind + '" data-openspec-id="' + esc(id) + '">\\u270E Open in Specs \\u2197</button></div>';
6082
+ }
5988
6083
 
5989
6084
  function renderPanel() {
5990
6085
  var head = '', body = '';
@@ -6007,7 +6102,8 @@ var MODEL = __MODEL_JSON__;
6007
6102
  + (scopeFocus ? staticChip('current view') : '')
6008
6103
  + chip(c.subsystem, 'subsystem', c.subsystem)
6009
6104
  + (scopeFocus ? '' : openViewButton('component', c.id, c.owns.length > 0))
6010
- + openApiButton(componentExposesApi(c), c.apiTag);
6105
+ + openApiButton(componentExposesApi(c), c.apiTag)
6106
+ + openSpecButton('component', c.id);
6011
6107
 
6012
6108
  var linkedTypes = MODEL.types.filter(function (t) { return t.componentClass === c.id; });
6013
6109
  if (linkedTypes.length) {
@@ -6091,7 +6187,8 @@ var MODEL = __MODEL_JSON__;
6091
6187
  MODEL.types.forEach(function (t2) { if (t2.id === focusId) ty = t2; });
6092
6188
  if (ty) {
6093
6189
  head = '<h2>' + esc(ty.name) + '</h2>' + staticChip('\\u00AB' + ty.kind + '\\u00BB')
6094
- + (ty.subsystem ? chip(ty.subsystem, 'subsystem', ty.subsystem) : staticChip('system-level shared'));
6190
+ + (ty.subsystem ? chip(ty.subsystem, 'subsystem', ty.subsystem) : staticChip('system-level shared'))
6191
+ + openSpecButton('type', ty.id);
6095
6192
 
6096
6193
  if (ty.componentClass) {
6097
6194
  head += '<div style="margin-top:6px"><b style="font-size:11px">Class Component:</b> ' + chip(ty.componentClass, 'component', ty.componentClass) + '</div>';
@@ -6167,7 +6264,8 @@ var MODEL = __MODEL_JSON__;
6167
6264
  + (s.status ? staticChip(s.status) : '')
6168
6265
  + (scopeFocus ? staticChip('current view') : '')
6169
6266
  + (scopeFocus ? '' : openViewButton('subsystem', s.id, subKids > 0))
6170
- + openApiButton(subsystemExposesApi(s.id), '');
6267
+ + openApiButton(subsystemExposesApi(s.id), '')
6268
+ + openSpecButton('subsystem', s.id);
6171
6269
  body += '<p class="desc">' + esc(s.description) + '</p>';
6172
6270
  if (s.trustedLinks.length) {
6173
6271
  body += section('Trusted links (fast lanes)', s.trustedLinks.length, s.trustedLinks.map(function (t2) {
@@ -6225,6 +6323,16 @@ var MODEL = __MODEL_JSON__;
6225
6323
  });
6226
6324
  })(oapis[oi]);
6227
6325
  }
6326
+ var ospecs = panel.querySelectorAll('[data-openspec-kind]');
6327
+ for (var si = 0; si < ospecs.length; si++) {
6328
+ (function (b) {
6329
+ b.addEventListener('click', function () {
6330
+ if (typeof opts !== 'undefined' && opts && typeof opts.onOpenSpec === 'function') {
6331
+ opts.onOpenSpec(b.getAttribute('data-openspec-kind'), b.getAttribute('data-openspec-id') || '');
6332
+ }
6333
+ });
6334
+ })(ospecs[si]);
6335
+ }
6228
6336
  var flows = panel.querySelectorAll('[data-flow-comp]');
6229
6337
  for (var j = 0; j < flows.length; j++) {
6230
6338
  (function (b) {
@@ -6237,6 +6345,23 @@ var MODEL = __MODEL_JSON__;
6237
6345
  }
6238
6346
 
6239
6347
  renderPanel();
6348
+ // Stage G: a deep link may focus a component and/or open a method's narrative
6349
+ // modal once the seeded view + DOM + cy graph exist. The host parses the URL hash
6350
+ // into opts.initialSelect / opts.initialFlow \u2014 both carry the component id, so this
6351
+ // works whether the seeded view is the component itself or its parent subsystem
6352
+ // (a leaf component has no meaningful "inside", so Specs deep-links open the parent
6353
+ // and focus the component here).
6354
+ (function () {
6355
+ if (typeof opts === 'undefined' || !opts) return;
6356
+ var f = opts.initialFlow, s = opts.initialSelect;
6357
+ var focusComp = (f && f.comp) || (s && s.comp);
6358
+ if (focusComp && compById[focusComp]) {
6359
+ try { select('component', focusComp, true); } catch (e) { /* ignore */ }
6360
+ }
6361
+ if (f && f.comp && f.method && compById[f.comp]) {
6362
+ try { openFlow(f.comp, f.method, f.mode === 'steps' ? 'steps' : 'flow'); } catch (e) { /* ignore */ }
6363
+ }
6364
+ })();
6240
6365
  })();
6241
6366
  </script>
6242
6367
  </body>
@@ -12618,6 +12743,28 @@ function buildGraphModel(level) {
12618
12743
  ...t.subsystem ? { parentId: t.subsystem } : {}
12619
12744
  });
12620
12745
  }
12746
+ const componentByInterface = /* @__PURE__ */ new Map();
12747
+ for (const c of model.components) {
12748
+ for (const intf of c.interfaces) componentByInterface.set(intf.id, c.id);
12749
+ }
12750
+ for (const impl of loadImplementationSpecs()) {
12751
+ const componentId = componentByInterface.get(impl.contract);
12752
+ if (!componentId) continue;
12753
+ nodes.push({
12754
+ id: impl.id,
12755
+ label: impl.name,
12756
+ kind: "implementation",
12757
+ level: 4,
12758
+ parentId: componentId,
12759
+ ...impl.status ? { status: impl.status } : {}
12760
+ });
12761
+ candidates.push({ from: componentId, to: impl.id, edgeKind: "owns" });
12762
+ }
12763
+ for (const c of model.components) {
12764
+ for (const memberId of c.owns) {
12765
+ candidates.push({ from: c.id, to: memberId, edgeKind: "owns" });
12766
+ }
12767
+ }
12621
12768
  for (const e of model.edges) {
12622
12769
  candidates.push({ from: e.from, to: e.to, edgeKind: "depends_on" });
12623
12770
  }
@@ -27583,6 +27730,7 @@ function removeIdentityProviderRecord(dataDir, id) {
27583
27730
  }
27584
27731
  var PROJECT_CREATE_CAPABILITY = "project:create";
27585
27732
  var PROJECT_WRITE_CAPABILITY = "project:write";
27733
+ var PROJECT_READ_CAPABILITY = "project:read";
27586
27734
  var POLICY_MANAGE_CAPABILITY = "project:admin";
27587
27735
  function carriesInstancePermission(cfg, principal, capability) {
27588
27736
  return authorize(cfg.dataDir, principal, capability, "instance", "").value === "yes";
@@ -27647,6 +27795,19 @@ function recordProjectProfileSelection(root, selection) {
27647
27795
  writeYamlFile(AI_PATHS.projectConfig(), raw);
27648
27796
  });
27649
27797
  }
27798
+ function readProjectType(root) {
27799
+ return runWithProjectRoot(root, () => {
27800
+ const raw = readYamlFile(AI_PATHS.projectConfig());
27801
+ return raw?.projectType ?? "backend";
27802
+ });
27803
+ }
27804
+ function writeProjectType(root, projectType) {
27805
+ runWithProjectRoot(root, () => {
27806
+ const raw = readYamlFile(AI_PATHS.projectConfig()) ?? {};
27807
+ raw["projectType"] = projectType;
27808
+ writeYamlFile(AI_PATHS.projectConfig(), raw);
27809
+ });
27810
+ }
27650
27811
  function requestPackNames(request) {
27651
27812
  const sel = request.profileSelection;
27652
27813
  if (!sel) return [];
@@ -27841,6 +28002,32 @@ function reconcileProjectPolicy(cfg, credential, projectId) {
27841
28002
  );
27842
28003
  return result;
27843
28004
  }
28005
+ function getProjectConfig(cfg, credential, projectId) {
28006
+ const principal = requirePrincipal4(cfg, credential);
28007
+ if (authorize(cfg.dataDir, principal, PROJECT_READ_CAPABILITY, "project", projectId).value !== "yes") {
28008
+ throw new ForbiddenError(
28009
+ "reading a project's configuration requires project:read over the project"
28010
+ );
28011
+ }
28012
+ const root = resolveProjectRoot(cfg.dataDir, principal, projectId);
28013
+ if (!root) throw new Error(`Unknown project "${projectId}".`);
28014
+ const projectType = readProjectType(root);
28015
+ const locked = runWithProjectRoot(root, () => hostCore.readLockRecord() !== null);
28016
+ return { projectType, locked };
28017
+ }
28018
+ function setProjectType(cfg, credential, projectId, projectType) {
28019
+ const principal = requirePrincipal4(cfg, credential);
28020
+ if (authorize(cfg.dataDir, principal, PROJECT_WRITE_CAPABILITY, "project", projectId).value !== "yes") {
28021
+ throw new ForbiddenError(
28022
+ "changing a project's configuration requires project:write over the project"
28023
+ );
28024
+ }
28025
+ const root = resolveProjectRoot(cfg.dataDir, principal, projectId);
28026
+ if (!root) throw new Error(`Unknown project "${projectId}".`);
28027
+ writeProjectType(root, projectType);
28028
+ const locked = runWithProjectRoot(root, () => hostCore.readLockRecord() !== null);
28029
+ return { projectType, locked };
28030
+ }
27844
28031
  function getPackPolicy(cfg, credential) {
27845
28032
  requirePrincipal4(cfg, credential);
27846
28033
  return getPackPolicyRecord(cfg.dataDir) ?? PERMISSIVE_DEFAULT_POLICY;
@@ -27904,7 +28091,7 @@ function handlePolicyRequest(cfg, credential, req, res, body, url) {
27904
28091
 
27905
28092
  // src/server/identity.ts
27906
28093
  var PROJECT_ADMIN_CAPABILITY2 = "project:admin";
27907
- var PROJECT_READ_CAPABILITY = "project:read";
28094
+ var PROJECT_READ_CAPABILITY2 = "project:read";
27908
28095
  var PROJECT_WRITE_CAPABILITY2 = "project:write";
27909
28096
  function requireInstanceProjectAdmin(cfg, principal, what) {
27910
28097
  if (authorize(cfg.dataDir, principal, PROJECT_ADMIN_CAPABILITY2, "instance", "").value !== "yes") {
@@ -28050,7 +28237,7 @@ function revokeToken(cfg, credential, tokenId) {
28050
28237
  }
28051
28238
  function mintSelfToken(cfg, credential, projectId, write) {
28052
28239
  const principal = requirePrincipal5(cfg, credential);
28053
- if (authorize(cfg.dataDir, principal, PROJECT_READ_CAPABILITY, "project", projectId).value !== "yes") {
28240
+ if (authorize(cfg.dataDir, principal, PROJECT_READ_CAPABILITY2, "project", projectId).value !== "yes") {
28054
28241
  throw new ForbiddenError("caller lacks project:read on the requested project");
28055
28242
  }
28056
28243
  if (write && authorize(cfg.dataDir, principal, PROJECT_WRITE_CAPABILITY2, "project", projectId).value !== "yes") {
@@ -28696,7 +28883,7 @@ function audienceDistance(resolution, targetProjectId) {
28696
28883
  // src/server/landscape.ts
28697
28884
  var yamlLib = __toESM(require("js-yaml"));
28698
28885
  var PROJECT_ADMIN_CAPABILITY3 = "project:admin";
28699
- var PROJECT_READ_CAPABILITY2 = "project:read";
28886
+ var PROJECT_READ_CAPABILITY3 = "project:read";
28700
28887
  var PROJECT_WRITE_CAPABILITY3 = "project:write";
28701
28888
  function requirePrincipal6(cfg, credential) {
28702
28889
  const principal = authenticateCredential(cfg.dataDir, credential);
@@ -28710,7 +28897,7 @@ function readView(cfg, principal) {
28710
28897
  if (isInstanceAdmin(principal)) {
28711
28898
  return { all: true, projectIds: /* @__PURE__ */ new Set(), unitIds: /* @__PURE__ */ new Set(), contextUnitIds: /* @__PURE__ */ new Set() };
28712
28899
  }
28713
- const read = visibleScopes(cfg.dataDir, principal, PROJECT_READ_CAPABILITY2);
28900
+ const read = visibleScopes(cfg.dataDir, principal, PROJECT_READ_CAPABILITY3);
28714
28901
  const admin = visibleScopes(cfg.dataDir, principal, PROJECT_ADMIN_CAPABILITY3);
28715
28902
  const unitIds = /* @__PURE__ */ new Set([...actionableUnitIds(read), ...actionableUnitIds(admin)]);
28716
28903
  const contextUnitIds = new Set(
@@ -28725,13 +28912,13 @@ function readView(cfg, principal) {
28725
28912
  }
28726
28913
  function requireLandscapeReach(cfg, principal, view, what) {
28727
28914
  if (view.all || view.projectIds.size > 0 || view.unitIds.size > 0) return;
28728
- if (permitsCap(cfg, principal, PROJECT_READ_CAPABILITY2, "instance", "") || permitsCap(cfg, principal, PROJECT_ADMIN_CAPABILITY3, "instance", "")) {
28915
+ if (permitsCap(cfg, principal, PROJECT_READ_CAPABILITY3, "instance", "") || permitsCap(cfg, principal, PROJECT_ADMIN_CAPABILITY3, "instance", "")) {
28729
28916
  return;
28730
28917
  }
28731
28918
  throw new ForbiddenError(`${what} requires project:read reach`);
28732
28919
  }
28733
28920
  function requireObserverScope(cfg, principal, observerProjectId) {
28734
- const covered = permitsCap(cfg, principal, PROJECT_READ_CAPABILITY2, "project", observerProjectId) || permitsCap(cfg, principal, PROJECT_WRITE_CAPABILITY3, "project", observerProjectId);
28921
+ const covered = permitsCap(cfg, principal, PROJECT_READ_CAPABILITY3, "project", observerProjectId) || permitsCap(cfg, principal, PROJECT_WRITE_CAPABILITY3, "project", observerProjectId);
28735
28922
  if (!covered) {
28736
28923
  throw new ForbiddenError("caller lacks scope over the current project");
28737
28924
  }
@@ -30789,6 +30976,12 @@ function evaluateProjectPolicy2(cfg, credential, projectId) {
30789
30976
  function reconcileProjectPolicy2(cfg, credential, projectId) {
30790
30977
  return reconcileProjectPolicy(cfg, credential, projectId);
30791
30978
  }
30979
+ function getProjectConfig2(cfg, credential, projectId) {
30980
+ return getProjectConfig(cfg, credential, projectId);
30981
+ }
30982
+ function setProjectType2(cfg, credential, projectId, projectType) {
30983
+ return setProjectType(cfg, credential, projectId, projectType);
30984
+ }
30792
30985
  function listProducers2(cfg, credential, project2) {
30793
30986
  return listProducers(cfg, credential, project2);
30794
30987
  }
@@ -34091,6 +34284,12 @@ function opsPolicyEvaluate(cfg, sessionId, url, res) {
34091
34284
  function opsPolicyReconcile(cfg, sessionId, body, res) {
34092
34285
  sendJson(res, 200, reconcileProjectPolicy2(cfg, sessionId, String(body?.projectId ?? "")));
34093
34286
  }
34287
+ function opsGetProjectConfig(cfg, sessionId, url, res) {
34288
+ sendJson(res, 200, getProjectConfig2(cfg, sessionId, q(url, "projectId") ?? ""));
34289
+ }
34290
+ function opsSetProjectConfig(cfg, sessionId, body, res) {
34291
+ sendJson(res, 200, setProjectType2(cfg, sessionId, String(body?.projectId ?? ""), String(body?.projectType ?? "")));
34292
+ }
34094
34293
  function opsListProducers(cfg, sessionId, url, res) {
34095
34294
  sendJson(res, 200, { producers: listProducers2(cfg, sessionId, q(url, "projectId") ?? "") });
34096
34295
  }
@@ -34323,6 +34522,12 @@ async function handleWebRequest(cfg, req, res, body, url, ctx) {
34323
34522
  if (req.method === "POST" && parts.length === 4 && parts[2] === "packs" && parts[3] === "adopt") {
34324
34523
  return opsAdoptProjectPack(cfg, sessionId, body, res);
34325
34524
  }
34525
+ if (req.method === "GET" && parts.length === 3 && parts[2] === "config") {
34526
+ return opsGetProjectConfig(cfg, sessionId, url, res);
34527
+ }
34528
+ if (req.method === "POST" && parts.length === 3 && parts[2] === "config") {
34529
+ return opsSetProjectConfig(cfg, sessionId, body, res);
34530
+ }
34326
34531
  if (req.method === "GET" && parts.length === 3 && parts[2] === "policy") {
34327
34532
  return opsPolicyEvaluate(cfg, sessionId, url, res);
34328
34533
  }