@mandujs/core 0.53.1 → 0.53.3

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.
@@ -57,12 +57,13 @@ export function renderKitchenHTML(): string {
57
57
  </div>
58
58
  </section>
59
59
 
60
- <nav class="tabs">
61
- <button class="tab" data-panel="activity">Activity</button>
62
- <button class="tab active" data-panel="routes">Routes</button>
63
- <button class="tab" data-panel="guard">Guard</button>
64
- <button class="tab" data-panel="preview">Preview</button>
65
- <button class="tab" data-panel="contracts">Contracts</button>
60
+ <nav class="tabs">
61
+ <button class="tab" data-panel="activity">Activity</button>
62
+ <button class="tab" data-panel="agent">Agent</button>
63
+ <button class="tab active" data-panel="routes">Routes</button>
64
+ <button class="tab" data-panel="guard">Guard</button>
65
+ <button class="tab" data-panel="preview">Preview</button>
66
+ <button class="tab" data-panel="contracts">Contracts</button>
66
67
  <button class="tab" data-panel="requests">Requests</button>
67
68
  <button class="tab" data-panel="mcp-activity">MCP</button>
68
69
  <button class="tab" data-panel="cache">Cache</button>
@@ -70,24 +71,37 @@ export function renderKitchenHTML(): string {
70
71
  </nav>
71
72
 
72
73
  <main class="panels">
73
- <section id="panel-activity" class="panel">
74
- <div class="panel-header">
75
- <div>
76
- <h2>Activity Stream</h2>
77
- <p class="panel-subtitle">Recent Kitchen events and MCP activity.</p>
74
+ <section id="panel-activity" class="panel">
75
+ <div class="panel-header">
76
+ <div>
77
+ <h2>Activity Stream</h2>
78
+ <p class="panel-subtitle">Recent Kitchen events and MCP activity.</p>
78
79
  </div>
79
80
  <button id="clear-activity" class="btn-sm">Clear</button>
80
81
  </div>
81
82
  <div id="activity-list" class="activity-list">
82
83
  <div class="empty-state">Waiting for MCP activity...</div>
83
- </div>
84
- </section>
85
-
86
- <section id="panel-routes" class="panel active">
87
- <div class="panel-header">
88
- <div>
89
- <h2>Routes</h2>
90
- <p class="panel-subtitle">Current filesystem routes, slots, contracts, and hydration hints.</p>
84
+ </div>
85
+ </section>
86
+
87
+ <section id="panel-agent" class="panel">
88
+ <div class="panel-header">
89
+ <div>
90
+ <h2>Agent Supervisor</h2>
91
+ <p class="panel-subtitle">Situation brief, tool routing, prompt pack, and next safe action for supervised agents.</p>
92
+ </div>
93
+ <button id="refresh-agent" class="btn-sm">Refresh</button>
94
+ </div>
95
+ <div id="agent-content" class="agent-content">
96
+ <div class="empty-state">Open Agent to build a context pack.</div>
97
+ </div>
98
+ </section>
99
+
100
+ <section id="panel-routes" class="panel active">
101
+ <div class="panel-header">
102
+ <div>
103
+ <h2>Routes</h2>
104
+ <p class="panel-subtitle">Current filesystem routes, slots, contracts, and hydration hints.</p>
91
105
  </div>
92
106
  <div id="routes-summary" class="summary"></div>
93
107
  </div>
@@ -1188,11 +1202,141 @@ const CSS = /* css */ `
1188
1202
  box-shadow: 0 12px 28px rgba(30, 42, 58, 0.22);
1189
1203
  }
1190
1204
 
1191
- .requests-list, .mcp-list, .cache-content, .metrics-content {
1192
- display: flex;
1193
- flex-direction: column;
1194
- gap: 6px;
1195
- }
1205
+ .requests-list, .mcp-list, .cache-content, .metrics-content, .agent-content {
1206
+ display: flex;
1207
+ flex-direction: column;
1208
+ gap: 6px;
1209
+ }
1210
+
1211
+ .agent-grid {
1212
+ display: grid;
1213
+ grid-template-columns: minmax(0, 1.1fr) minmax(320px, 0.9fr);
1214
+ gap: 14px;
1215
+ }
1216
+
1217
+ .agent-stack {
1218
+ display: flex;
1219
+ flex-direction: column;
1220
+ gap: 12px;
1221
+ }
1222
+
1223
+ .agent-card {
1224
+ padding: 14px;
1225
+ border: 1px solid var(--line);
1226
+ border-radius: 8px;
1227
+ background: rgba(255, 255, 255, 0.72);
1228
+ }
1229
+
1230
+ .agent-card-title {
1231
+ display: flex;
1232
+ align-items: center;
1233
+ justify-content: space-between;
1234
+ gap: 10px;
1235
+ margin-bottom: 8px;
1236
+ color: var(--ink);
1237
+ font-size: 14px;
1238
+ font-weight: 700;
1239
+ }
1240
+
1241
+ .agent-card-body {
1242
+ color: var(--muted);
1243
+ font-size: 13px;
1244
+ line-height: 1.55;
1245
+ }
1246
+
1247
+ .agent-pill-row {
1248
+ display: flex;
1249
+ flex-wrap: wrap;
1250
+ gap: 6px;
1251
+ margin-top: 10px;
1252
+ }
1253
+
1254
+ .agent-pill {
1255
+ padding: 4px 8px;
1256
+ border-radius: 6px;
1257
+ background: rgba(30, 42, 58, 0.08);
1258
+ color: var(--ink);
1259
+ font-family: "IBM Plex Mono", "Cascadia Code", monospace;
1260
+ font-size: 11px;
1261
+ }
1262
+
1263
+ .agent-severity {
1264
+ padding: 3px 7px;
1265
+ border-radius: 6px;
1266
+ font-size: 11px;
1267
+ font-weight: 700;
1268
+ text-transform: uppercase;
1269
+ }
1270
+
1271
+ .agent-severity.info { background: rgba(46, 102, 184, 0.12); color: var(--info); }
1272
+ .agent-severity.warn { background: rgba(173, 122, 18, 0.14); color: var(--warning); }
1273
+ .agent-severity.error { background: rgba(188, 61, 61, 0.12); color: var(--danger); }
1274
+
1275
+ .agent-stat-grid {
1276
+ display: grid;
1277
+ grid-template-columns: repeat(4, minmax(0, 1fr));
1278
+ gap: 8px;
1279
+ margin-top: 12px;
1280
+ }
1281
+
1282
+ .agent-stat {
1283
+ padding: 10px;
1284
+ border-radius: 8px;
1285
+ border: 1px solid rgba(220, 207, 186, 0.7);
1286
+ background: rgba(255, 253, 250, 0.84);
1287
+ }
1288
+
1289
+ .agent-stat-label {
1290
+ color: var(--muted);
1291
+ font-size: 11px;
1292
+ }
1293
+
1294
+ .agent-stat-value {
1295
+ color: var(--ink);
1296
+ font-size: 19px;
1297
+ font-weight: 700;
1298
+ margin-top: 3px;
1299
+ }
1300
+
1301
+ .agent-rec {
1302
+ display: grid;
1303
+ grid-template-columns: 150px 1fr;
1304
+ gap: 10px;
1305
+ padding: 10px 0;
1306
+ border-top: 1px solid rgba(220, 207, 186, 0.65);
1307
+ }
1308
+
1309
+ .agent-rec:first-child {
1310
+ border-top: 0;
1311
+ padding-top: 0;
1312
+ }
1313
+
1314
+ .agent-rec-title {
1315
+ color: var(--ink);
1316
+ font-weight: 700;
1317
+ font-size: 13px;
1318
+ }
1319
+
1320
+ .agent-rec-detail {
1321
+ color: var(--muted);
1322
+ font-size: 12px;
1323
+ line-height: 1.5;
1324
+ }
1325
+
1326
+ .agent-prompt {
1327
+ width: 100%;
1328
+ min-height: 220px;
1329
+ margin-top: 10px;
1330
+ padding: 12px;
1331
+ resize: vertical;
1332
+ border-radius: 8px;
1333
+ border: 1px solid var(--line);
1334
+ background: #fffdfa;
1335
+ color: var(--ink);
1336
+ font-family: "IBM Plex Mono", "Cascadia Code", monospace;
1337
+ font-size: 12px;
1338
+ line-height: 1.45;
1339
+ }
1196
1340
 
1197
1341
  .req-row {
1198
1342
  display: grid;
@@ -1313,13 +1457,17 @@ const CSS = /* css */ `
1313
1457
  grid-template-columns: repeat(2, minmax(0, 1fr));
1314
1458
  }
1315
1459
 
1316
- .contracts-layout {
1317
- grid-template-columns: 1fr;
1318
- }
1319
-
1320
- .guard-summary {
1321
- grid-template-columns: repeat(2, minmax(0, 1fr));
1322
- }
1460
+ .contracts-layout {
1461
+ grid-template-columns: 1fr;
1462
+ }
1463
+
1464
+ .agent-grid {
1465
+ grid-template-columns: 1fr;
1466
+ }
1467
+
1468
+ .guard-summary {
1469
+ grid-template-columns: repeat(2, minmax(0, 1fr));
1470
+ }
1323
1471
  }
1324
1472
 
1325
1473
  @media (max-width: 720px) {
@@ -1345,12 +1493,20 @@ const CSS = /* css */ `
1345
1493
  width: calc(50% - 4px);
1346
1494
  justify-content: center;
1347
1495
  }
1348
-
1349
- .panel-header {
1350
- flex-direction: column;
1351
- }
1352
- }
1353
- `;
1496
+
1497
+ .panel-header {
1498
+ flex-direction: column;
1499
+ }
1500
+
1501
+ .agent-stat-grid {
1502
+ grid-template-columns: repeat(2, minmax(0, 1fr));
1503
+ }
1504
+
1505
+ .agent-rec {
1506
+ grid-template-columns: 1fr;
1507
+ }
1508
+ }
1509
+ `;
1354
1510
 
1355
1511
  // ─── JavaScript ──────────────────────────────────
1356
1512
 
@@ -1367,12 +1523,12 @@ const JS = /* js */ `
1367
1523
  console.log('[Kitchen]', msg);
1368
1524
  }
1369
1525
 
1370
- function escapeHtml(str) {
1371
- if (!str) return '';
1372
- var d = document.createElement('div');
1373
- d.appendChild(document.createTextNode(String(str)));
1374
- return d.innerHTML;
1375
- }
1526
+ function escapeHtml(str) {
1527
+ if (str === null || str === undefined) return '';
1528
+ var d = document.createElement('div');
1529
+ d.appendChild(document.createTextNode(String(str)));
1530
+ return d.innerHTML;
1531
+ }
1376
1532
 
1377
1533
  function setMetric(id, value) {
1378
1534
  var el = document.getElementById(id);
@@ -1893,9 +2049,10 @@ const JS = /* js */ `
1893
2049
 
1894
2050
  loadContracts();
1895
2051
 
1896
- // ─── Requests Tab ─────────────────────────────
1897
- var requestsListEl = document.getElementById('requests-list');
1898
- var requestsDetailEl = document.getElementById('requests-detail');
2052
+ // ─── Requests Tab ─────────────────────────────
2053
+ var requestsListEl = document.getElementById('requests-list');
2054
+ var requestsDetailEl = document.getElementById('requests-detail');
2055
+ var agentContentEl = document.getElementById('agent-content');
1899
2056
 
1900
2057
  function fetchJson(url, cb) {
1901
2058
  var xhr = new XMLHttpRequest();
@@ -1910,16 +2067,142 @@ const JS = /* js */ `
1910
2067
  xhr.send();
1911
2068
  }
1912
2069
 
1913
- function statusClass(s) {
1914
- if (!s) return '';
1915
- if (s >= 500) return 's5';
1916
- if (s >= 400) return 's4';
1917
- if (s >= 300) return 's3';
1918
- return 's2';
1919
- }
1920
-
1921
- function loadRequests() {
1922
- requestsListEl.innerHTML = '<div class="empty-state">Loading requests...</div>';
2070
+ function statusClass(s) {
2071
+ if (!s) return '';
2072
+ if (s >= 500) return 's5';
2073
+ if (s >= 400) return 's4';
2074
+ if (s >= 300) return 's3';
2075
+ return 's2';
2076
+ }
2077
+
2078
+ // ─── Agent Supervisor Tab ─────────────────────
2079
+ function renderAgentPills(items) {
2080
+ if (!items || !items.length) return '';
2081
+ var html = '<div class="agent-pill-row">';
2082
+ for (var i = 0; i < items.length; i++) {
2083
+ html += '<span class="agent-pill">' + escapeHtml(items[i]) + '</span>';
2084
+ }
2085
+ return html + '</div>';
2086
+ }
2087
+
2088
+ function renderAgentDetails(items) {
2089
+ if (!items || !items.length) return '';
2090
+ var html = '<ul>';
2091
+ for (var i = 0; i < items.length; i++) {
2092
+ html += '<li>' + escapeHtml(items[i]) + '</li>';
2093
+ }
2094
+ return html + '</ul>';
2095
+ }
2096
+
2097
+ function renderAgentContext(data) {
2098
+ var situation = data.situation || {};
2099
+ var summary = data.summary || {};
2100
+ var routes = summary.routes || {};
2101
+ var status = data.agentStatus || {};
2102
+ var brain = status.brain || {};
2103
+ var action = data.nextSafeAction || {};
2104
+ var prompt = data.prompt || {};
2105
+ var recs = data.toolRecommendations || [];
2106
+ var cards = data.knowledgeCards || [];
2107
+ var promptText = prompt.copyText || '';
2108
+
2109
+ var html = '<div class="agent-grid">' +
2110
+ '<div class="agent-stack">' +
2111
+ '<div class="agent-card">' +
2112
+ '<div class="agent-card-title">' +
2113
+ '<span>' + escapeHtml(situation.title || 'No situation') + '</span>' +
2114
+ '<span class="agent-severity ' + escapeHtml(situation.severity || 'info') + '">' + escapeHtml(situation.category || 'agent') + '</span>' +
2115
+ '</div>' +
2116
+ '<div class="agent-card-body">' + renderAgentDetails(situation.details || []) + '</div>' +
2117
+ '<div class="agent-stat-grid">' +
2118
+ '<div class="agent-stat"><div class="agent-stat-label">Routes</div><div class="agent-stat-value">' + escapeHtml(routes.total || 0) + '</div></div>' +
2119
+ '<div class="agent-stat"><div class="agent-stat-label">Islands</div><div class="agent-stat-value">' + escapeHtml(routes.islands || 0) + '</div></div>' +
2120
+ '<div class="agent-stat"><div class="agent-stat-label">Contracts</div><div class="agent-stat-value">' + escapeHtml(routes.contracts || 0) + '</div></div>' +
2121
+ '<div class="agent-stat"><div class="agent-stat-label">MCP Calls</div><div class="agent-stat-value">' + escapeHtml(status.observedToolCalls || 0) + '</div></div>' +
2122
+ '</div>' +
2123
+ '</div>' +
2124
+ '<div class="agent-card">' +
2125
+ '<div class="agent-card-title"><span>Next Safe Action</span><span class="agent-severity info">' + escapeHtml(action.mode || 'observe') + '</span></div>' +
2126
+ '<div class="agent-card-body"><strong>' + escapeHtml(action.title || '-') + '</strong><br>' + escapeHtml(action.reason || '') + '</div>' +
2127
+ renderAgentPills([action.tool || '', action.command || ''].filter(Boolean)) +
2128
+ renderAgentPills(action.validation || []) +
2129
+ '</div>' +
2130
+ '<div class="agent-card">' +
2131
+ '<div class="agent-card-title"><span>Prompt Pack</span><button id="copy-agent-prompt" class="btn-sm">Copy</button></div>' +
2132
+ '<textarea id="agent-prompt-text" class="agent-prompt" readonly>' + escapeHtml(promptText) + '</textarea>' +
2133
+ '</div>' +
2134
+ '</div>' +
2135
+ '<div class="agent-stack">' +
2136
+ '<div class="agent-card">' +
2137
+ '<div class="agent-card-title"><span>Brain</span><span class="agent-severity info">' + escapeHtml(brain.oauth || 'unknown') + '</span></div>' +
2138
+ '<div class="agent-card-body">' + escapeHtml(brain.note || '') + '</div>' +
2139
+ renderAgentPills([brain.statusTool || 'mandu.brain.status']) +
2140
+ '</div>' +
2141
+ '<div class="agent-card">' +
2142
+ '<div class="agent-card-title"><span>Tool Router</span></div>';
2143
+
2144
+ for (var i = 0; i < recs.length; i++) {
2145
+ var r = recs[i];
2146
+ html += '<div class="agent-rec">' +
2147
+ '<div class="agent-rec-title">' + escapeHtml(r.skill) + '</div>' +
2148
+ '<div class="agent-rec-detail">' +
2149
+ '<strong>' + escapeHtml(r.task) + '</strong><br>' +
2150
+ escapeHtml(r.useWhen) +
2151
+ renderAgentPills(r.mcpTools || []) +
2152
+ '<div style="margin-top:6px;">Fallback: <code>' + escapeHtml(r.cliFallback) + '</code></div>' +
2153
+ '</div>' +
2154
+ '</div>';
2155
+ }
2156
+
2157
+ html += '</div><div class="agent-card">' +
2158
+ '<div class="agent-card-title"><span>Knowledge</span></div>';
2159
+
2160
+ for (var c = 0; c < cards.length; c++) {
2161
+ var card = cards[c];
2162
+ html += '<div class="agent-rec">' +
2163
+ '<div class="agent-rec-title">' + escapeHtml(card.title) + '</div>' +
2164
+ '<div class="agent-rec-detail">' + escapeHtml(card.body) + renderAgentPills(card.references || []) + '</div>' +
2165
+ '</div>';
2166
+ }
2167
+
2168
+ html += '</div></div></div>';
2169
+ agentContentEl.innerHTML = html;
2170
+
2171
+ var copyBtn = document.getElementById('copy-agent-prompt');
2172
+ if (copyBtn) {
2173
+ copyBtn.addEventListener('click', function() {
2174
+ var textEl = document.getElementById('agent-prompt-text');
2175
+ var text = textEl ? textEl.value : promptText;
2176
+ if (navigator.clipboard && navigator.clipboard.writeText) {
2177
+ navigator.clipboard.writeText(text).then(function() {
2178
+ copyBtn.textContent = 'Copied';
2179
+ setTimeout(function() { copyBtn.textContent = 'Copy'; }, 900);
2180
+ }).catch(function() {
2181
+ textEl && textEl.select();
2182
+ });
2183
+ } else if (textEl) {
2184
+ textEl.select();
2185
+ }
2186
+ });
2187
+ }
2188
+ }
2189
+
2190
+ function loadAgentContext() {
2191
+ if (!agentContentEl) return;
2192
+ agentContentEl.innerHTML = '<div class="empty-state">Building agent context...</div>';
2193
+ fetchJson('/__kitchen/api/agent-context', function(err, data) {
2194
+ if (err) {
2195
+ agentContentEl.innerHTML = '<div class="empty-state">Failed: ' + escapeHtml(err.message) + '</div>';
2196
+ return;
2197
+ }
2198
+ renderAgentContext(data);
2199
+ });
2200
+ }
2201
+
2202
+ document.getElementById('refresh-agent').addEventListener('click', loadAgentContext);
2203
+
2204
+ function loadRequests() {
2205
+ requestsListEl.innerHTML = '<div class="empty-state">Loading requests...</div>';
1923
2206
  fetchJson('/__kitchen/api/requests?limit=100', function(err, data) {
1924
2207
  if (err) {
1925
2208
  requestsListEl.innerHTML = '<div class="empty-state">Failed: ' + escapeHtml(err.message) + '</div>';
@@ -2116,12 +2399,13 @@ const JS = /* js */ `
2116
2399
  document.getElementById('refresh-metrics').addEventListener('click', loadMetrics);
2117
2400
 
2118
2401
  // Lazy-load new tab data when clicked (avoids loading everything up front)
2119
- var tabLoaders = {
2120
- 'requests': loadRequests,
2121
- 'mcp-activity': loadMcpActivity,
2122
- 'cache': loadCacheStats,
2123
- 'metrics': loadMetrics
2124
- };
2402
+ var tabLoaders = {
2403
+ 'agent': loadAgentContext,
2404
+ 'requests': loadRequests,
2405
+ 'mcp-activity': loadMcpActivity,
2406
+ 'cache': loadCacheStats,
2407
+ 'metrics': loadMetrics
2408
+ };
2125
2409
  var loadedTabs = {};
2126
2410
  for (var t = 0; t < tabs.length; t++) {
2127
2411
  tabs[t].addEventListener('click', function() {
@@ -574,7 +574,7 @@ describe("generateRepoSource — emission shape", () => {
574
574
  expect(generateRepoSource(nonPersistent, { enable: false })).toBeNull();
575
575
  });
576
576
 
577
- test("postgres/sqlite emit INSERT ... RETURNING, mysql emits INSERT + SELECT LAST_INSERT_ID", () => {
577
+ test("postgres/sqlite emit INSERT ... RETURNING, mysql emits INSERT + primary-key re-select", () => {
578
578
  // Assert on SQL in the emitted code rather than on comments —
579
579
  // comments reference "RETURNING" as prose across all three providers.
580
580
  // The discriminator we check is the actual SQL keyword on an insert
@@ -585,20 +585,37 @@ describe("generateRepoSource — emission shape", () => {
585
585
  const sqlite = generateRepoSource(persistentResource("user", "sqlite"))!;
586
586
  expect(sqlite).toMatch(/INSERT INTO[\s\S]*?RETURNING/);
587
587
 
588
- const mysql = generateRepoSource(persistentResource("user", "mysql"))!;
589
- expect(mysql).not.toMatch(/INSERT INTO[\s\S]*?RETURNING/);
590
- expect(mysql).toContain("LAST_INSERT_ID()");
591
- });
592
-
593
- test("mysql repo uses backtick identifiers, postgres uses double quotes", () => {
594
- const mysql = generateRepoSource(persistentResource("user", "mysql"))!;
595
- expect(mysql).toContain("`users`");
596
- expect(mysql).not.toContain(`"users"`);
597
-
598
- const pg = generateRepoSource(persistentResource("user", "postgres"))!;
599
- expect(pg).toContain(`"users"`);
600
- expect(pg).not.toContain("`users`");
601
- });
588
+ const mysql = generateRepoSource(persistentResource("user", "mysql"))!;
589
+ expect(mysql).not.toMatch(/INSERT INTO[\s\S]*?RETURNING/);
590
+ expect(mysql).toContain("WHERE \\`id\\` = ${input.id}");
591
+ expect(mysql).not.toContain("WHERE \\`id\\` = LAST_INSERT_ID()");
592
+ });
593
+
594
+ test("mysql repo escapes backtick identifiers for generated template literals, postgres uses double quotes", () => {
595
+ const mysql = generateRepoSource(persistentResource("user", "mysql"))!;
596
+ expect(mysql).toContain("\\`users\\`");
597
+ expect(mysql).not.toContain(`"users"`);
598
+
599
+ const pg = generateRepoSource(persistentResource("user", "postgres"))!;
600
+ expect(pg).toContain(`"users"`);
601
+ expect(pg).not.toContain("\\`users\\`");
602
+ });
603
+
604
+ test("create input includes primary key unless the primary key has a DB default", () => {
605
+ const callerSuppliedPk = generateRepoSource(persistentResource("user", "postgres"))!;
606
+ expect(callerSuppliedPk).toContain("async create(input: User)");
607
+ expect(callerSuppliedPk).toContain("${input.id}");
608
+ expect(callerSuppliedPk).not.toContain('async create(input: Omit<User, "id">)');
609
+
610
+ const dbGeneratedPk = persistentResource("token", "postgres");
611
+ dbGeneratedPk.definition.fields.id = {
612
+ ...dbGeneratedPk.definition.fields.id,
613
+ default: "gen_random_uuid()",
614
+ };
615
+ const src = generateRepoSource(dbGeneratedPk)!;
616
+ expect(src).toContain('async create(input: Omit<Token, "id">)');
617
+ expect(src).not.toMatch(/INSERT INTO[\s\S]*"id"[\s\S]*VALUES/);
618
+ });
602
619
 
603
620
  test("shouldEmitRepo returns true only when persistence is declared", () => {
604
621
  const persistent = persistentResource("user");
@@ -419,6 +419,30 @@ describe("emitChange — add-column", () => {
419
419
  expect(emitChange(change, "sqlite"))
420
420
  .toBe('ALTER TABLE "users" ADD COLUMN "age" REAL;');
421
421
  });
422
+
423
+ test("indexed:true field emits ADD COLUMN plus auto CREATE INDEX", () => {
424
+ const indexed: Change = {
425
+ kind: "add-column",
426
+ resourceName: "users",
427
+ field: field({ name: "role", type: "string", indexed: true }),
428
+ };
429
+
430
+ expect(emitChange(indexed, "postgres")).toBe(
431
+ [
432
+ 'ALTER TABLE "users" ADD COLUMN "role" TEXT NOT NULL;',
433
+ 'CREATE INDEX "idx_users_role" ON "users" ("role");',
434
+ ].join("\n"),
435
+ );
436
+ });
437
+
438
+ test("indexed:true unique or primary add-column does not create redundant index", () => {
439
+ const unique: Change = {
440
+ kind: "add-column",
441
+ resourceName: "users",
442
+ field: field({ name: "slug", type: "string", indexed: true, unique: true }),
443
+ };
444
+ expect(emitChange(unique, "postgres")).not.toContain("idx_users_slug");
445
+ });
422
446
  });
423
447
 
424
448
  describe("emitChange — drop-column", () => {
@@ -345,7 +345,18 @@ function emitAddColumn(
345
345
  ): string {
346
346
  const table = quoteIdent(resourceName, provider);
347
347
  const columnDef = emitColumnDef(field, provider);
348
- return `ALTER TABLE ${table} ADD COLUMN ${columnDef};`;
348
+ const addColumn = `ALTER TABLE ${table} ADD COLUMN ${columnDef};`;
349
+ if (!field.indexed || field.unique || field.primary) return addColumn;
350
+ return [
351
+ addColumn,
352
+ emitCreateIndex(
353
+ resourceName,
354
+ autoIndexName(resourceName, field.name),
355
+ [field.name],
356
+ false,
357
+ provider,
358
+ ),
359
+ ].join("\n");
349
360
  }
350
361
 
351
362
  /**