@aion0/forge 0.10.39 → 0.10.41
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/CLAUDE.md +1 -1
- package/RELEASE_NOTES.md +83 -6
- package/app/api/bridge-info/route.ts +34 -0
- package/app/api/connectors/[id]/test/route.ts +14 -0
- package/app/api/connectors/import-config-template/route.ts +103 -13
- package/app/api/enterprise-keys/route.ts +204 -0
- package/app/api/marketplace/sync-all/route.ts +28 -0
- package/app/api/monitor/route.ts +29 -6
- package/app/api/onboarding/route.ts +897 -23
- package/app/api/projects/clone/route.ts +51 -0
- package/app/api/settings/route.ts +11 -2
- package/bin/forge-server.mjs +189 -30
- package/cli/mw.mjs +16 -6
- package/cli/mw.ts +19 -6
- package/components/ConnectorsPanel.tsx +85 -13
- package/components/CraftTerminal.tsx +12 -3
- package/components/Dashboard.tsx +55 -17
- package/components/DocTerminal.tsx +12 -6
- package/components/EnterpriseBadge.tsx +420 -0
- package/components/LoginStatusPanel.tsx +15 -1
- package/components/OnboardingWizard.tsx +418 -31
- package/components/SettingsModal.tsx +382 -63
- package/components/SkillsPanel.tsx +116 -91
- package/components/WebTerminal.tsx +36 -13
- package/dev-test.sh +34 -1
- package/install.sh +29 -2
- package/lib/agents/claude-adapter.ts +18 -4
- package/lib/agents/index.ts +33 -4
- package/lib/auth/login-status.ts +14 -0
- package/lib/chat/agent-loop.ts +23 -1
- package/lib/chat/protocols/http.ts +15 -2
- package/lib/chat/tool-dispatcher.ts +163 -1
- package/lib/connectors/registry.ts +69 -4
- package/lib/connectors/sync.ts +536 -138
- package/lib/connectors/test-runner.ts +21 -3
- package/lib/connectors/types.ts +36 -4
- package/lib/connectors/wizard-template.ts +161 -0
- package/lib/dirs.ts +5 -0
- package/lib/enterprise-known.ts +34 -0
- package/lib/enterprise-secret.ts +87 -0
- package/lib/enterprise.ts +208 -0
- package/lib/help-docs/00-overview.md +12 -0
- package/lib/help-docs/01-settings.md +47 -1
- package/lib/help-docs/17-connectors.md +25 -22
- package/lib/help-docs/CLAUDE.md +1 -0
- package/lib/init.ts +13 -6
- package/lib/marketplace-sync.ts +70 -0
- package/lib/memory/temper-provision.ts +92 -0
- package/lib/pipeline-gc.ts +5 -2
- package/lib/pipeline.ts +26 -21
- package/lib/plugins/templates.ts +76 -3
- package/lib/projects.ts +85 -0
- package/lib/settings.ts +10 -0
- package/lib/telegram-bot.ts +14 -2
- package/lib/workflow-marketplace.ts +174 -108
- package/package.json +1 -1
- package/{middleware.ts → proxy.ts} +2 -1
- package/src/core/db/database.ts +8 -2
- package/templates/connector-config-template.json +0 -7
|
@@ -47,9 +47,19 @@ function SecretChangeDialog({ field, label, isSet, onSave, onClose }: {
|
|
|
47
47
|
const [error, setError] = useState('');
|
|
48
48
|
const [saving, setSaving] = useState(false);
|
|
49
49
|
|
|
50
|
+
// First-time admin password bootstrap: when the user is setting the
|
|
51
|
+
// admin password itself AND none exists yet, there's nothing to verify
|
|
52
|
+
// against — hide the "Admin password" input and just collect the new
|
|
53
|
+
// value. Backend mirrors the bypass.
|
|
54
|
+
const isFirstAdminSet = field === 'telegramTunnelPassword' && !isSet;
|
|
55
|
+
|
|
50
56
|
const canSave = mode === 'clear'
|
|
51
57
|
? adminPassword.length > 0
|
|
52
|
-
: (
|
|
58
|
+
: (
|
|
59
|
+
(isFirstAdminSet || adminPassword.length > 0)
|
|
60
|
+
&& newValue.length > 0
|
|
61
|
+
&& newValue === confirmValue
|
|
62
|
+
);
|
|
53
63
|
|
|
54
64
|
const handleSave = async () => {
|
|
55
65
|
if (mode === 'change' && newValue !== confirmValue) {
|
|
@@ -95,15 +105,22 @@ function SecretChangeDialog({ field, label, isSet, onSave, onClose }: {
|
|
|
95
105
|
)}
|
|
96
106
|
</div>
|
|
97
107
|
|
|
98
|
-
|
|
99
|
-
<
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
108
|
+
{!isFirstAdminSet && (
|
|
109
|
+
<div className="space-y-1">
|
|
110
|
+
<label className="text-[10px] text-[var(--text-secondary)]">Admin password (login password)</label>
|
|
111
|
+
<SecretInput
|
|
112
|
+
value={adminPassword}
|
|
113
|
+
onChange={v => { setAdminPassword(v); setError(''); }}
|
|
114
|
+
placeholder="Enter login password to verify"
|
|
115
|
+
className={inputClass}
|
|
116
|
+
/>
|
|
117
|
+
</div>
|
|
118
|
+
)}
|
|
119
|
+
{isFirstAdminSet && (
|
|
120
|
+
<p className="text-[10px] text-[var(--text-secondary)]">
|
|
121
|
+
No admin password set yet — skipping verification. Pick one below.
|
|
122
|
+
</p>
|
|
123
|
+
)}
|
|
107
124
|
|
|
108
125
|
{mode === 'change' && (
|
|
109
126
|
<>
|
|
@@ -346,6 +363,41 @@ export default function SettingsModal({ onClose }: { onClose: () => void }) {
|
|
|
346
363
|
>
|
|
347
364
|
<h2 className="text-sm font-bold">Settings</h2>
|
|
348
365
|
|
|
366
|
+
{/* Identity — display name, email, company, dept. At the top
|
|
367
|
+
because every other config (connector usernames, Jenkins
|
|
368
|
+
inject_params, tunnel ownership) derives from these via
|
|
369
|
+
{user.*} / {dept.*} tokens. Company + dept are auto-filled
|
|
370
|
+
by the wizard when picking an enterprise template; changing
|
|
371
|
+
dept here triggers a re-apply of that dept's template. */}
|
|
372
|
+
<div className="grid grid-cols-2 gap-2">
|
|
373
|
+
<div className="space-y-1">
|
|
374
|
+
<label className="text-xs text-[var(--text-secondary)] font-semibold uppercase">Display Name</label>
|
|
375
|
+
<input
|
|
376
|
+
type="text"
|
|
377
|
+
value={(settings as any).displayName || ''}
|
|
378
|
+
onChange={e => setSettings({ ...settings, displayName: e.target.value } as any)}
|
|
379
|
+
placeholder="Forge"
|
|
380
|
+
className="w-full px-2 py-1 bg-[var(--bg-tertiary)] border border-[var(--border)] rounded text-xs text-[var(--text-primary)]"
|
|
381
|
+
/>
|
|
382
|
+
</div>
|
|
383
|
+
<div className="space-y-1">
|
|
384
|
+
<label className="text-xs text-[var(--text-secondary)] font-semibold uppercase">Email</label>
|
|
385
|
+
<input
|
|
386
|
+
type="email"
|
|
387
|
+
value={(settings as any).displayEmail || ''}
|
|
388
|
+
onChange={e => setSettings({ ...settings, displayEmail: e.target.value } as any)}
|
|
389
|
+
placeholder="local@forge"
|
|
390
|
+
className="w-full px-2 py-1 bg-[var(--bg-tertiary)] border border-[var(--border)] rounded text-xs text-[var(--text-primary)]"
|
|
391
|
+
/>
|
|
392
|
+
</div>
|
|
393
|
+
</div>
|
|
394
|
+
<ProfileCompanyDeptRow settings={settings} setSettings={setSettings} />
|
|
395
|
+
|
|
396
|
+
{/* Marketplace Providers (enterprise repo keys) — right after
|
|
397
|
+
identity because most users who care about settings at all
|
|
398
|
+
are here to manage / inspect enterprise sources. */}
|
|
399
|
+
<MarketplaceProvidersSection />
|
|
400
|
+
|
|
349
401
|
{/* Project Roots */}
|
|
350
402
|
<div className="space-y-2">
|
|
351
403
|
<label className="text-xs text-[var(--text-secondary)] font-semibold uppercase">
|
|
@@ -803,59 +855,6 @@ export default function SettingsModal({ onClose }: { onClose: () => void }) {
|
|
|
803
855
|
|
|
804
856
|
</div>
|
|
805
857
|
|
|
806
|
-
{/* Display Name */}
|
|
807
|
-
<div className="space-y-2">
|
|
808
|
-
<label className="text-xs text-[var(--text-secondary)] font-semibold uppercase">
|
|
809
|
-
Display Name
|
|
810
|
-
</label>
|
|
811
|
-
<input
|
|
812
|
-
type="text"
|
|
813
|
-
value={(settings as any).displayName || ''}
|
|
814
|
-
onChange={e => setSettings({ ...settings, displayName: e.target.value } as any)}
|
|
815
|
-
placeholder="Forge"
|
|
816
|
-
className="w-full px-2 py-1 bg-[var(--bg-tertiary)] border border-[var(--border)] rounded text-xs text-[var(--text-primary)]"
|
|
817
|
-
/>
|
|
818
|
-
</div>
|
|
819
|
-
|
|
820
|
-
{/* Email */}
|
|
821
|
-
<div className="space-y-2">
|
|
822
|
-
<label className="text-xs text-[var(--text-secondary)] font-semibold uppercase">
|
|
823
|
-
Email
|
|
824
|
-
</label>
|
|
825
|
-
<input
|
|
826
|
-
type="email"
|
|
827
|
-
value={(settings as any).displayEmail || ''}
|
|
828
|
-
onChange={e => setSettings({ ...settings, displayEmail: e.target.value } as any)}
|
|
829
|
-
placeholder="local@forge"
|
|
830
|
-
className="w-full px-2 py-1 bg-[var(--bg-tertiary)] border border-[var(--border)] rounded text-xs text-[var(--text-primary)]"
|
|
831
|
-
/>
|
|
832
|
-
</div>
|
|
833
|
-
|
|
834
|
-
{/* Re-run Onboarding */}
|
|
835
|
-
<div className="space-y-2">
|
|
836
|
-
<label className="text-xs text-[var(--text-secondary)] font-semibold uppercase">
|
|
837
|
-
Onboarding
|
|
838
|
-
</label>
|
|
839
|
-
<button
|
|
840
|
-
type="button"
|
|
841
|
-
onClick={async () => {
|
|
842
|
-
await fetch('/api/onboarding', {
|
|
843
|
-
method: 'POST',
|
|
844
|
-
headers: { 'Content-Type': 'application/json' },
|
|
845
|
-
body: JSON.stringify({ action: 'reset' }),
|
|
846
|
-
});
|
|
847
|
-
// Reload so the Dashboard banner re-renders.
|
|
848
|
-
window.location.reload();
|
|
849
|
-
}}
|
|
850
|
-
className="text-[11px] px-2.5 py-1 border border-[var(--accent)] text-[var(--accent)] rounded hover:bg-[var(--accent)] hover:text-white"
|
|
851
|
-
>
|
|
852
|
-
↺ Re-run Onboarding wizard
|
|
853
|
-
</button>
|
|
854
|
-
<p className="text-[10px] text-[var(--text-secondary)]">
|
|
855
|
-
Re-opens the first-run setup banner. Existing values are preserved — the wizard never overwrites non-empty fields.
|
|
856
|
-
</p>
|
|
857
|
-
</div>
|
|
858
|
-
|
|
859
858
|
{/* Admin Password */}
|
|
860
859
|
<div className="space-y-2">
|
|
861
860
|
<label className="text-xs text-[var(--text-secondary)] font-semibold uppercase">
|
|
@@ -1356,6 +1355,326 @@ function AddProfileForm({ type, baseAgents, onAdd }: {
|
|
|
1356
1355
|
);
|
|
1357
1356
|
}
|
|
1358
1357
|
|
|
1358
|
+
/**
|
|
1359
|
+
* Company + Dept dropdowns for the Identity block. Pure profile fields
|
|
1360
|
+
* — editing them just updates the label. The wizard does the actual
|
|
1361
|
+
* template apply on first setup; to re-pick a template the user goes
|
|
1362
|
+
* through Re-run wizard.
|
|
1363
|
+
*/
|
|
1364
|
+
function ProfileCompanyDeptRow({ settings, setSettings }: { settings: any; setSettings: (s: any) => void }) {
|
|
1365
|
+
const [sources, setSources] = useState<EnterpriseSourceView[]>([]);
|
|
1366
|
+
useEffect(() => {
|
|
1367
|
+
fetch('/api/enterprise-keys').then(r => r.json()).then(j => setSources(j.sources || [])).catch(() => {});
|
|
1368
|
+
}, []);
|
|
1369
|
+
const currentCompany: string = settings?.company || '';
|
|
1370
|
+
const sourceForCompany = sources.find(s => s.display_name === currentCompany);
|
|
1371
|
+
return (
|
|
1372
|
+
<div className="space-y-1">
|
|
1373
|
+
<div className="grid grid-cols-2 gap-2">
|
|
1374
|
+
<div className="space-y-1">
|
|
1375
|
+
<label className="text-xs text-[var(--text-secondary)] font-semibold uppercase">Company</label>
|
|
1376
|
+
<select
|
|
1377
|
+
value={currentCompany}
|
|
1378
|
+
onChange={e => setSettings({ ...settings, company: e.target.value, dept: '' })}
|
|
1379
|
+
className="w-full px-2 py-1 bg-[var(--bg-tertiary)] border border-[var(--border)] rounded text-xs text-[var(--text-primary)] focus:outline-none focus:border-[var(--accent)]"
|
|
1380
|
+
>
|
|
1381
|
+
<option value="">(none — public)</option>
|
|
1382
|
+
{sources.map(s => (
|
|
1383
|
+
<option key={s.tenant_id} value={s.display_name}>{s.display_name}</option>
|
|
1384
|
+
))}
|
|
1385
|
+
</select>
|
|
1386
|
+
</div>
|
|
1387
|
+
<div className="space-y-1">
|
|
1388
|
+
<label className="text-xs text-[var(--text-secondary)] font-semibold uppercase">Department</label>
|
|
1389
|
+
<select
|
|
1390
|
+
value={settings?.dept || ''}
|
|
1391
|
+
disabled={!sourceForCompany?.departments?.length}
|
|
1392
|
+
onChange={e => setSettings({ ...settings, dept: e.target.value })}
|
|
1393
|
+
className="w-full px-2 py-1 bg-[var(--bg-tertiary)] border border-[var(--border)] rounded text-xs text-[var(--text-primary)] focus:outline-none focus:border-[var(--accent)] disabled:opacity-50"
|
|
1394
|
+
>
|
|
1395
|
+
<option value="">{sourceForCompany?.departments?.length ? '(pick a dept…)' : '(no depts)'}</option>
|
|
1396
|
+
{sourceForCompany?.departments?.map(d => (
|
|
1397
|
+
<option key={d.id} value={d.display_name}>{d.display_name}</option>
|
|
1398
|
+
))}
|
|
1399
|
+
</select>
|
|
1400
|
+
</div>
|
|
1401
|
+
</div>
|
|
1402
|
+
<p className="text-[10px] text-[var(--text-secondary)]">
|
|
1403
|
+
Auto-filled by the wizard. Editing here just updates the label — to re-apply a different dept's template, use ↺ Re-run wizard below.
|
|
1404
|
+
</p>
|
|
1405
|
+
</div>
|
|
1406
|
+
);
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
interface EnterpriseSourceView {
|
|
1410
|
+
tenant_id: string;
|
|
1411
|
+
display_name: string;
|
|
1412
|
+
repo_url: string;
|
|
1413
|
+
priority: number;
|
|
1414
|
+
pat_preview: string;
|
|
1415
|
+
last_sync?: { ok: boolean; error?: string; fetched_at: string } | null;
|
|
1416
|
+
departments?: Array<{ id: string; display_name: string; has_template?: boolean }>;
|
|
1417
|
+
active_dept?: string | null;
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
/**
|
|
1421
|
+
* Marketplace Providers — manage the enterprise keys that unlock private
|
|
1422
|
+
* connector / workflow registries. See lib/enterprise.ts for the on-disk
|
|
1423
|
+
* format. UI surface: list of configured sources (priority order) + add
|
|
1424
|
+
* field + remove button. Re-order is intentionally not exposed yet —
|
|
1425
|
+
* `remove + add again` is good enough for Phase 1 and avoids the modal
|
|
1426
|
+
* drag-and-drop complexity; revisit if real users want it.
|
|
1427
|
+
*/
|
|
1428
|
+
function MarketplaceProvidersSection() {
|
|
1429
|
+
const [sources, setSources] = useState<EnterpriseSourceView[]>([]);
|
|
1430
|
+
const [loading, setLoading] = useState(true);
|
|
1431
|
+
const [adding, setAdding] = useState(false);
|
|
1432
|
+
const [newKey, setNewKey] = useState('');
|
|
1433
|
+
const [error, setError] = useState('');
|
|
1434
|
+
const [status, setStatus] = useState('');
|
|
1435
|
+
// "+ Add another enterprise tenant" is collapsed by default — only one
|
|
1436
|
+
// key is typical, and the input doesn't belong next to the sync /
|
|
1437
|
+
// reinstall actions (those operate on EXISTING sources). Auto-open
|
|
1438
|
+
// when there are zero sources so first-time users still see it.
|
|
1439
|
+
const [showAddTenant, setShowAddTenant] = useState(false);
|
|
1440
|
+
|
|
1441
|
+
const load = useCallback(async () => {
|
|
1442
|
+
setLoading(true);
|
|
1443
|
+
try {
|
|
1444
|
+
const r = await fetch('/api/enterprise-keys');
|
|
1445
|
+
const data = await r.json();
|
|
1446
|
+
setSources(data?.sources || []);
|
|
1447
|
+
} catch (e) {
|
|
1448
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
1449
|
+
} finally {
|
|
1450
|
+
setLoading(false);
|
|
1451
|
+
}
|
|
1452
|
+
}, []);
|
|
1453
|
+
|
|
1454
|
+
useEffect(() => { load(); }, [load]);
|
|
1455
|
+
// Reveal the Add-tenant input automatically when there are no
|
|
1456
|
+
// sources at all — otherwise it stays tucked behind a disclosure.
|
|
1457
|
+
useEffect(() => {
|
|
1458
|
+
if (!loading && sources.length === 0) setShowAddTenant(true);
|
|
1459
|
+
}, [loading, sources.length]);
|
|
1460
|
+
|
|
1461
|
+
const handleAdd = async () => {
|
|
1462
|
+
const key = newKey.trim();
|
|
1463
|
+
if (!key) return;
|
|
1464
|
+
setError('');
|
|
1465
|
+
setStatus('');
|
|
1466
|
+
setAdding(true);
|
|
1467
|
+
try {
|
|
1468
|
+
const r = await fetch('/api/enterprise-keys', {
|
|
1469
|
+
method: 'POST',
|
|
1470
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1471
|
+
body: JSON.stringify({ key }),
|
|
1472
|
+
});
|
|
1473
|
+
const data = await r.json();
|
|
1474
|
+
if (!data.ok) {
|
|
1475
|
+
setError(data.error || 'failed to add key');
|
|
1476
|
+
} else {
|
|
1477
|
+
setNewKey('');
|
|
1478
|
+
setStatus(`✓ Added ${data.tenant_id} — marketplace synced`);
|
|
1479
|
+
await load();
|
|
1480
|
+
}
|
|
1481
|
+
} catch (e) {
|
|
1482
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
1483
|
+
} finally {
|
|
1484
|
+
setAdding(false);
|
|
1485
|
+
}
|
|
1486
|
+
};
|
|
1487
|
+
|
|
1488
|
+
const [resyncing, setResyncing] = useState(false);
|
|
1489
|
+
|
|
1490
|
+
// Comprehensive sync: connectors (registry + manifests + wizard
|
|
1491
|
+
// templates) + workflows (pipelines + recipes) + skills, from every
|
|
1492
|
+
// configured source (public + each enterprise tenant). Same endpoint
|
|
1493
|
+
// as Marketplace top → ↻ Sync all so both views agree.
|
|
1494
|
+
// User: "settings 的 re-sync 是同步所有的数据的".
|
|
1495
|
+
const handleResync = async () => {
|
|
1496
|
+
setError('');
|
|
1497
|
+
setStatus('');
|
|
1498
|
+
setResyncing(true);
|
|
1499
|
+
try {
|
|
1500
|
+
const r = await fetch('/api/marketplace/sync-all', { method: 'POST' });
|
|
1501
|
+
const data = await r.json();
|
|
1502
|
+
if (!data.ok && data.error) { setError(data.error); }
|
|
1503
|
+
else {
|
|
1504
|
+
const parts: string[] = [];
|
|
1505
|
+
if (data.connectors) parts.push(`connectors ${data.connectors.sources_ok ?? 0}/${data.connectors.sources_total ?? 0} sources, ${data.connectors.manifests_refreshed ?? 0} manifests`);
|
|
1506
|
+
if (data.workflows) parts.push(`workflows ${data.workflows.pipelines ?? 0} pipelines + ${data.workflows.recipes ?? 0} recipes`);
|
|
1507
|
+
if (data.skills) parts.push(`skills ${data.skills.synced ?? 0}`);
|
|
1508
|
+
setStatus(`✓ Synced — ${parts.join(' · ')}`);
|
|
1509
|
+
}
|
|
1510
|
+
await load();
|
|
1511
|
+
} catch (e) {
|
|
1512
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
1513
|
+
} finally {
|
|
1514
|
+
setResyncing(false);
|
|
1515
|
+
}
|
|
1516
|
+
};
|
|
1517
|
+
|
|
1518
|
+
const handleRemove = async (tenant_id: string) => {
|
|
1519
|
+
if (!confirm(`Remove enterprise source "${tenant_id}"?\n\nInstalled connectors stay on disk; they just stop receiving updates from this source.`)) return;
|
|
1520
|
+
setError('');
|
|
1521
|
+
setStatus('');
|
|
1522
|
+
try {
|
|
1523
|
+
const r = await fetch(`/api/enterprise-keys?tenant_id=${encodeURIComponent(tenant_id)}`, { method: 'DELETE' });
|
|
1524
|
+
const data = await r.json();
|
|
1525
|
+
if (!data.ok) { setError(data.error || 'remove failed'); return; }
|
|
1526
|
+
setStatus(`✓ Removed ${tenant_id}`);
|
|
1527
|
+
await load();
|
|
1528
|
+
} catch (e) {
|
|
1529
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
1530
|
+
}
|
|
1531
|
+
};
|
|
1532
|
+
|
|
1533
|
+
return (
|
|
1534
|
+
<div className="space-y-2">
|
|
1535
|
+
<label className="text-xs text-[var(--text-secondary)] font-semibold uppercase">
|
|
1536
|
+
Enterprise
|
|
1537
|
+
</label>
|
|
1538
|
+
<p className="text-[10px] text-[var(--text-secondary)]">
|
|
1539
|
+
Private enterprise registries layered on top of the public marketplace. Higher in this list = higher priority (overrides the public one when the same id exists). Keys are encrypted at rest.
|
|
1540
|
+
</p>
|
|
1541
|
+
|
|
1542
|
+
{/* Configured sources */}
|
|
1543
|
+
<div className="rounded border border-[var(--border)] divide-y divide-[var(--border)] bg-[var(--bg-tertiary)]">
|
|
1544
|
+
{loading ? (
|
|
1545
|
+
<div className="px-3 py-2 text-[11px] text-[var(--text-secondary)]">Loading…</div>
|
|
1546
|
+
) : sources.length === 0 ? (
|
|
1547
|
+
<div className="px-3 py-2 text-[11px] text-[var(--text-secondary)] italic">
|
|
1548
|
+
No enterprise sources configured. Public marketplace only.
|
|
1549
|
+
</div>
|
|
1550
|
+
) : (
|
|
1551
|
+
sources.map((s) => {
|
|
1552
|
+
const sync = s.last_sync;
|
|
1553
|
+
const synced = sync?.ok;
|
|
1554
|
+
const failed = sync && !sync.ok;
|
|
1555
|
+
return (
|
|
1556
|
+
<div key={s.tenant_id} className="px-3 py-2 flex flex-col gap-1">
|
|
1557
|
+
<div className="flex items-center gap-2">
|
|
1558
|
+
<span className="text-[10px] px-1.5 py-0.5 rounded bg-amber-500/15 text-amber-400 shrink-0">
|
|
1559
|
+
🔒 {s.display_name}
|
|
1560
|
+
</span>
|
|
1561
|
+
<div className="flex-1 min-w-0">
|
|
1562
|
+
<div className="text-[11px] font-mono text-[var(--text-primary)] truncate" title={s.repo_url}>
|
|
1563
|
+
{s.repo_url}
|
|
1564
|
+
</div>
|
|
1565
|
+
<div className="text-[9px] text-[var(--text-secondary)] mt-0.5 flex items-center gap-2">
|
|
1566
|
+
<span>priority {s.priority} · PAT {s.pat_preview}</span>
|
|
1567
|
+
{synced && <span className="text-emerald-500">● synced</span>}
|
|
1568
|
+
{failed && <span className="text-red-400">✗ sync failed</span>}
|
|
1569
|
+
{!sync && <span className="italic">(no sync yet)</span>}
|
|
1570
|
+
</div>
|
|
1571
|
+
{/* Dept selection lives in the Identity row above —
|
|
1572
|
+
it's a profile property, not a tenant property. */}
|
|
1573
|
+
</div>
|
|
1574
|
+
<button
|
|
1575
|
+
onClick={() => handleRemove(s.tenant_id)}
|
|
1576
|
+
className="text-[10px] px-2 py-0.5 rounded border border-red-500/30 text-red-400 hover:bg-red-500/10"
|
|
1577
|
+
>
|
|
1578
|
+
Remove
|
|
1579
|
+
</button>
|
|
1580
|
+
</div>
|
|
1581
|
+
{failed && sync?.error && (
|
|
1582
|
+
<div className="text-[10px] text-red-300 border border-red-500/30 bg-red-500/5 rounded px-2 py-1 leading-snug whitespace-pre-wrap break-words">
|
|
1583
|
+
{sync.error}
|
|
1584
|
+
</div>
|
|
1585
|
+
)}
|
|
1586
|
+
</div>
|
|
1587
|
+
);
|
|
1588
|
+
})
|
|
1589
|
+
)}
|
|
1590
|
+
</div>
|
|
1591
|
+
|
|
1592
|
+
<div className="flex gap-2">
|
|
1593
|
+
<button
|
|
1594
|
+
onClick={handleResync}
|
|
1595
|
+
disabled={resyncing}
|
|
1596
|
+
className="text-[11px] px-3 py-1.5 rounded border border-[var(--border)] text-[var(--text-primary)] hover:bg-[var(--bg-tertiary)] disabled:opacity-40"
|
|
1597
|
+
title="Sync EVERYTHING from every source (public + each enterprise tenant): connectors + workflows + skills + wizard templates. Same as Marketplace top → Sync all."
|
|
1598
|
+
>
|
|
1599
|
+
{resyncing ? 'Syncing…' : '↻ Sync all'}
|
|
1600
|
+
</button>
|
|
1601
|
+
<button
|
|
1602
|
+
type="button"
|
|
1603
|
+
onClick={async () => {
|
|
1604
|
+
await fetch('/api/onboarding', {
|
|
1605
|
+
method: 'POST',
|
|
1606
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1607
|
+
body: JSON.stringify({ action: 'reset' }),
|
|
1608
|
+
});
|
|
1609
|
+
window.location.reload();
|
|
1610
|
+
}}
|
|
1611
|
+
className="text-[11px] px-3 py-1.5 rounded border border-[var(--accent)] text-[var(--accent)] hover:bg-[var(--accent)] hover:text-white"
|
|
1612
|
+
title="Re-run the onboarding wizard to swap tenant / dept or re-apply template defaults."
|
|
1613
|
+
>
|
|
1614
|
+
↺ Re-run wizard
|
|
1615
|
+
</button>
|
|
1616
|
+
</div>
|
|
1617
|
+
|
|
1618
|
+
{/* Status / error */}
|
|
1619
|
+
{error && (
|
|
1620
|
+
<div className="text-[10px] text-red-400 border border-red-500/30 bg-red-500/5 rounded px-2 py-1">
|
|
1621
|
+
{error}
|
|
1622
|
+
</div>
|
|
1623
|
+
)}
|
|
1624
|
+
{status && !error && (
|
|
1625
|
+
<div className="text-[10px] text-green-400">{status}</div>
|
|
1626
|
+
)}
|
|
1627
|
+
|
|
1628
|
+
<p className="text-[9px] text-[var(--text-secondary)] italic">
|
|
1629
|
+
Removing a source keeps its installed connectors/workflows on disk — they just stop getting updates from that source.
|
|
1630
|
+
</p>
|
|
1631
|
+
|
|
1632
|
+
{/* Add another tenant — separate concept from sync/reinstall.
|
|
1633
|
+
Collapsed by default since most users only ever have one. */}
|
|
1634
|
+
<div className="border-t border-[var(--border)] pt-2 mt-1">
|
|
1635
|
+
{!showAddTenant ? (
|
|
1636
|
+
<button
|
|
1637
|
+
onClick={() => setShowAddTenant(true)}
|
|
1638
|
+
className="text-[10px] text-[var(--text-secondary)] hover:text-[var(--text-primary)]"
|
|
1639
|
+
>
|
|
1640
|
+
+ Add another enterprise tenant
|
|
1641
|
+
</button>
|
|
1642
|
+
) : (
|
|
1643
|
+
<div className="space-y-1.5">
|
|
1644
|
+
<label className="text-[10px] text-[var(--text-secondary)] block">
|
|
1645
|
+
Add another enterprise tenant (different company / org). Already-configured tenants show above — this is only for layering ANOTHER private registry on top.
|
|
1646
|
+
</label>
|
|
1647
|
+
<div className="flex gap-2">
|
|
1648
|
+
<input
|
|
1649
|
+
type="text"
|
|
1650
|
+
placeholder="acme:github_pat_xxx or github_pat_xxx@github.com/org/forge-enterprise-agent-yourco"
|
|
1651
|
+
value={newKey}
|
|
1652
|
+
onChange={(e) => setNewKey(e.target.value)}
|
|
1653
|
+
onKeyDown={(e) => { if (e.key === 'Enter' && !adding) handleAdd(); }}
|
|
1654
|
+
className="flex-1 text-[11px] font-mono px-2 py-1.5 rounded bg-[var(--bg-primary)] border border-[var(--border)] text-[var(--text-primary)] placeholder:text-[var(--text-secondary)]/50"
|
|
1655
|
+
/>
|
|
1656
|
+
<button
|
|
1657
|
+
onClick={handleAdd}
|
|
1658
|
+
disabled={adding || !newKey.trim()}
|
|
1659
|
+
className="text-[11px] px-3 py-1.5 rounded bg-[var(--accent)] text-white hover:opacity-90 disabled:opacity-40"
|
|
1660
|
+
>
|
|
1661
|
+
{adding ? 'Adding…' : 'Add tenant'}
|
|
1662
|
+
</button>
|
|
1663
|
+
<button
|
|
1664
|
+
onClick={() => { setShowAddTenant(false); setNewKey(''); }}
|
|
1665
|
+
className="text-[10px] px-2 py-1.5 text-[var(--text-secondary)] hover:text-[var(--text-primary)]"
|
|
1666
|
+
>
|
|
1667
|
+
Cancel
|
|
1668
|
+
</button>
|
|
1669
|
+
</div>
|
|
1670
|
+
</div>
|
|
1671
|
+
)}
|
|
1672
|
+
</div>
|
|
1673
|
+
|
|
1674
|
+
</div>
|
|
1675
|
+
);
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1359
1678
|
function AgentsSection({ settings, setSettings }: { settings: any; setSettings: (s: any) => void }) {
|
|
1360
1679
|
const { registry: modelsRegistry } = useModelsRegistry();
|
|
1361
1680
|
const [agents, setAgents] = useState<AgentEntry[]>([]);
|