@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
package/lib/telegram-bot.ts
CHANGED
|
@@ -1570,7 +1570,13 @@ async function sendNoteToDocsClaude(chatId: number, content: string) {
|
|
|
1570
1570
|
await new Promise(r => setTimeout(r, 500));
|
|
1571
1571
|
// cd to doc root and start claude
|
|
1572
1572
|
const sf = settings.skipPermissions ? ' --dangerously-skip-permissions' : '';
|
|
1573
|
-
|
|
1573
|
+
// Resolve absolute claude path — bare `claude` rides on tmux pane
|
|
1574
|
+
// PATH which can pick a stale global install. See
|
|
1575
|
+
// feedback_terminal_launch memory.
|
|
1576
|
+
const { resolveTerminalLaunch } = await import('./agents');
|
|
1577
|
+
const launch = resolveTerminalLaunch('claude', 'telegram');
|
|
1578
|
+
const claudeCmd = `"${launch.cliCmd}"`;
|
|
1579
|
+
spawnSync('tmux', ['send-keys', '-t', SESSION_NAME, `cd "${docRoot}" && ${claudeCmd} -c${sf}`, 'Enter'], { timeout: 5000 });
|
|
1574
1580
|
// Wait for Claude to start up
|
|
1575
1581
|
await new Promise(r => setTimeout(r, 3000));
|
|
1576
1582
|
await send(chatId, '🚀 Auto-started Docs Claude session.');
|
|
@@ -1590,7 +1596,13 @@ async function sendNoteToDocsClaude(chatId: number, content: string) {
|
|
|
1590
1596
|
if (paneCmd === 'zsh' || paneCmd === 'bash' || paneCmd === 'fish' || !paneCmd) {
|
|
1591
1597
|
try {
|
|
1592
1598
|
const sf = settings.skipPermissions ? ' --dangerously-skip-permissions' : '';
|
|
1593
|
-
|
|
1599
|
+
// Resolve absolute claude path — bare `claude` rides on tmux pane
|
|
1600
|
+
// PATH which can pick a stale global install. See
|
|
1601
|
+
// feedback_terminal_launch memory.
|
|
1602
|
+
const { resolveTerminalLaunch } = await import('./agents');
|
|
1603
|
+
const launch = resolveTerminalLaunch('claude', 'telegram');
|
|
1604
|
+
const claudeCmd = `"${launch.cliCmd}"`;
|
|
1605
|
+
spawnSync('tmux', ['send-keys', '-t', SESSION_NAME, `cd "${docRoot}" && ${claudeCmd} -c${sf}`, 'Enter'], { timeout: 5000 });
|
|
1594
1606
|
await new Promise(r => setTimeout(r, 3000));
|
|
1595
1607
|
await send(chatId, '🚀 Auto-started Claude in Docs session.');
|
|
1596
1608
|
} catch {
|
|
@@ -1,28 +1,45 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Workflow Marketplace — sync recipes + pipelines from
|
|
2
|
+
* Workflow Marketplace — sync recipes + pipelines from one or more Git
|
|
3
|
+
* repos. Mirrors the connector marketplace's multi-source model:
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
+
* Priority 0..N-1: Enterprise repos (the SAME repos used by connector
|
|
6
|
+
* sync — each enterprise registry.json declares its connectors AND
|
|
7
|
+
* its workflow templates). Workflow side reads the recipes/pipelines
|
|
8
|
+
* arrays out of the cache connector sync already wrote.
|
|
9
|
+
*
|
|
10
|
+
* Priority N (lowest): Public — settings.workflowRepoUrl (default
|
|
11
|
+
* aiwatching/forge-workflow). Fetched via raw.githubusercontent.com.
|
|
12
|
+
*
|
|
13
|
+
* Repo layout (same in public and enterprise):
|
|
5
14
|
*
|
|
6
15
|
* registry.json
|
|
7
|
-
* { version,
|
|
8
|
-
* recipes/<name>/recipe.yaml
|
|
9
|
-
* recipes/<name>/info.json
|
|
16
|
+
* { version, recipes: [...], pipelines: [...], (connectors: [...]) }
|
|
17
|
+
* recipes/<name>/recipe.yaml
|
|
18
|
+
* recipes/<name>/info.json (optional)
|
|
10
19
|
* pipelines/<name>/pipeline.yaml
|
|
11
|
-
* pipelines/<name>/info.json
|
|
20
|
+
* pipelines/<name>/info.json (optional)
|
|
12
21
|
*
|
|
13
|
-
* Local cache: <dataDir>/workflow-cache.json —
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
22
|
+
* Local cache: <dataDir>/workflow-cache.json — public registry only.
|
|
23
|
+
* Enterprise registries live in the connector cache; we read them via
|
|
24
|
+
* `readSourceRegistry` from lib/connectors/sync.ts. Installed status
|
|
25
|
+
* is derived by scanning files on disk under <dataDir>/recipes/ and
|
|
26
|
+
* <dataDir>/flows/ (handled by lib/jobs/recipes.ts and lib/pipeline.ts).
|
|
17
27
|
*/
|
|
18
28
|
|
|
19
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
29
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync } from 'node:fs';
|
|
20
30
|
import { join } from 'node:path';
|
|
21
31
|
import YAML from 'yaml';
|
|
22
32
|
import { getDataDir } from './dirs';
|
|
23
33
|
import { loadSettings } from './settings';
|
|
24
34
|
import { listRecipes } from './jobs/recipes';
|
|
25
35
|
import { listWorkflows } from './pipeline';
|
|
36
|
+
import {
|
|
37
|
+
listEnterpriseSourceMetas,
|
|
38
|
+
readSourceRegistry,
|
|
39
|
+
fetchSourceFile,
|
|
40
|
+
type SourceMeta,
|
|
41
|
+
type RegistryFile,
|
|
42
|
+
} from './connectors/sync';
|
|
26
43
|
|
|
27
44
|
export type WorkflowKind = 'recipe' | 'pipeline';
|
|
28
45
|
|
|
@@ -40,23 +57,43 @@ export interface MarketplaceEntry {
|
|
|
40
57
|
installed: boolean;
|
|
41
58
|
installed_version?: string;
|
|
42
59
|
has_update?: boolean;
|
|
60
|
+
/** Which marketplace source provided the winning entry. */
|
|
61
|
+
source_id?: string;
|
|
62
|
+
/** Other sources that also list this name but were outranked. */
|
|
63
|
+
hidden_sources?: { source_id: string; display_name: string; version: string }[];
|
|
43
64
|
}
|
|
44
65
|
|
|
45
66
|
const CACHE_FILE = () => join(getDataDir(), 'workflow-cache.json');
|
|
46
67
|
const REQUEST_TIMEOUT_MS = 10_000;
|
|
47
68
|
|
|
48
|
-
|
|
49
|
-
|
|
69
|
+
const DEFAULT_PUBLIC_REPO = 'https://raw.githubusercontent.com/aiwatching/forge-workflow/main';
|
|
70
|
+
|
|
71
|
+
function publicBaseUrl(): string {
|
|
72
|
+
return (loadSettings().workflowRepoUrl || '').trim().replace(/\/+$/, '') || DEFAULT_PUBLIC_REPO;
|
|
50
73
|
}
|
|
51
74
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
75
|
+
/** Public workflow source meta — separate repo from connectors, so we
|
|
76
|
+
* build it locally rather than reusing the connector sync's `public`. */
|
|
77
|
+
function publicWorkflowSource(priority: number): SourceMeta {
|
|
78
|
+
return {
|
|
79
|
+
id: 'public',
|
|
80
|
+
display_name: 'Public',
|
|
81
|
+
is_enterprise: false,
|
|
82
|
+
priority,
|
|
83
|
+
fetch_mode: 'raw',
|
|
84
|
+
base_url: publicBaseUrl(),
|
|
85
|
+
};
|
|
57
86
|
}
|
|
58
87
|
|
|
59
|
-
|
|
88
|
+
/** Ordered list of workflow sources, highest priority first. */
|
|
89
|
+
function listSources(): SourceMeta[] {
|
|
90
|
+
const enterprise = listEnterpriseSourceMetas();
|
|
91
|
+
return [...enterprise, publicWorkflowSource(enterprise.length)];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ─── Public cache I/O ──────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
function readPublicCache(): RegistryFile | null {
|
|
60
97
|
try {
|
|
61
98
|
const path = CACHE_FILE();
|
|
62
99
|
if (!existsSync(path)) return null;
|
|
@@ -64,15 +101,31 @@ function readCache(): RegistryFile | null {
|
|
|
64
101
|
} catch { return null; }
|
|
65
102
|
}
|
|
66
103
|
|
|
67
|
-
function
|
|
104
|
+
function writePublicCache(data: RegistryFile): void {
|
|
68
105
|
mkdirSync(getDataDir(), { recursive: true });
|
|
69
106
|
writeFileSync(CACHE_FILE(), JSON.stringify(data, null, 2), { mode: 0o600 });
|
|
70
107
|
}
|
|
71
108
|
|
|
109
|
+
function readSourceData(source: SourceMeta): RegistryFile | null {
|
|
110
|
+
if (source.is_enterprise) {
|
|
111
|
+
// Piggyback on connector sync's per-source cache — enterprise
|
|
112
|
+
// registry.json contains recipes/pipelines alongside connectors.
|
|
113
|
+
return readSourceRegistry(source.id);
|
|
114
|
+
}
|
|
115
|
+
return readPublicCache();
|
|
116
|
+
}
|
|
117
|
+
|
|
72
118
|
// ─── Sync ──────────────────────────────────────────────────
|
|
73
119
|
|
|
120
|
+
/**
|
|
121
|
+
* Refresh the workflow registry for every source whose registry we
|
|
122
|
+
* own. Enterprise registries are owned by the connector sync — those
|
|
123
|
+
* are pulled in `lib/connectors/sync.ts#syncRegistry`. This function
|
|
124
|
+
* just refreshes the public workflow registry; the UI typically
|
|
125
|
+
* triggers both syncs back-to-back.
|
|
126
|
+
*/
|
|
74
127
|
export async function syncMarketplace(): Promise<{ ok: boolean; recipes: number; pipelines: number; error?: string }> {
|
|
75
|
-
const base =
|
|
128
|
+
const base = publicBaseUrl();
|
|
76
129
|
if (!base) return { ok: false, recipes: 0, pipelines: 0, error: 'workflowRepoUrl is empty' };
|
|
77
130
|
const controller = new AbortController();
|
|
78
131
|
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
@@ -85,8 +138,8 @@ export async function syncMarketplace(): Promise<{ ok: boolean; recipes: number;
|
|
|
85
138
|
clearTimeout(timer);
|
|
86
139
|
if (!res.ok) return { ok: false, recipes: 0, pipelines: 0, error: `registry fetch failed: HTTP ${res.status}` };
|
|
87
140
|
const data = (await res.json()) as RegistryFile;
|
|
88
|
-
|
|
89
|
-
console.log(`[workflow-marketplace] synced ${data.recipes?.length ?? 0} recipes + ${data.pipelines?.length ?? 0} pipelines`);
|
|
141
|
+
writePublicCache(data);
|
|
142
|
+
console.log(`[workflow-marketplace] public synced ${data.recipes?.length ?? 0} recipes + ${data.pipelines?.length ?? 0} pipelines`);
|
|
90
143
|
return { ok: true, recipes: data.recipes?.length ?? 0, pipelines: data.pipelines?.length ?? 0 };
|
|
91
144
|
} catch (e) {
|
|
92
145
|
clearTimeout(timer);
|
|
@@ -96,7 +149,7 @@ export async function syncMarketplace(): Promise<{ ok: boolean; recipes: number;
|
|
|
96
149
|
}
|
|
97
150
|
}
|
|
98
151
|
|
|
99
|
-
// ─── List (merged
|
|
152
|
+
// ─── List (merged across sources) ──────────────────────────
|
|
100
153
|
|
|
101
154
|
function compareVersions(a: string, b: string): number {
|
|
102
155
|
const pa = (a || '0.0.0').split('.').map(Number);
|
|
@@ -108,34 +161,68 @@ function compareVersions(a: string, b: string): number {
|
|
|
108
161
|
return 0;
|
|
109
162
|
}
|
|
110
163
|
|
|
164
|
+
interface PendingEntry {
|
|
165
|
+
winning_source: SourceMeta;
|
|
166
|
+
raw: NonNullable<RegistryFile['recipes']>[number] | NonNullable<RegistryFile['pipelines']>[number];
|
|
167
|
+
hidden: { source_id: string; display_name: string; version: string }[];
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Walk sources in priority order; first occurrence wins, later ones
|
|
171
|
+
* collect into hidden_sources on the winning entry. */
|
|
172
|
+
function mergeAcrossSources(
|
|
173
|
+
kindKey: 'recipes' | 'pipelines',
|
|
174
|
+
sources: SourceMeta[],
|
|
175
|
+
): Map<string, PendingEntry> {
|
|
176
|
+
const out = new Map<string, PendingEntry>();
|
|
177
|
+
for (const source of sources) {
|
|
178
|
+
const data = readSourceData(source);
|
|
179
|
+
const items = (data?.[kindKey] || []) as NonNullable<RegistryFile['recipes']>;
|
|
180
|
+
for (const item of items) {
|
|
181
|
+
if (!item?.name) continue;
|
|
182
|
+
const existing = out.get(item.name);
|
|
183
|
+
if (!existing) {
|
|
184
|
+
out.set(item.name, { winning_source: source, raw: item, hidden: [] });
|
|
185
|
+
} else {
|
|
186
|
+
existing.hidden.push({
|
|
187
|
+
source_id: source.id,
|
|
188
|
+
display_name: source.display_name,
|
|
189
|
+
version: item.version || '0.0.0',
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return out;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function toEntry(kind: WorkflowKind, pe: PendingEntry): MarketplaceEntry {
|
|
198
|
+
const r = pe.raw;
|
|
199
|
+
return {
|
|
200
|
+
kind,
|
|
201
|
+
name: r.name,
|
|
202
|
+
display_name: r.display_name || r.name,
|
|
203
|
+
description: r.description,
|
|
204
|
+
version: r.version || '0.0.0',
|
|
205
|
+
author: r.author,
|
|
206
|
+
tags: r.tags,
|
|
207
|
+
score: r.score ?? 0,
|
|
208
|
+
rating: r.rating ?? 0,
|
|
209
|
+
source: 'registry',
|
|
210
|
+
source_id: pe.winning_source.id,
|
|
211
|
+
hidden_sources: pe.hidden.length ? pe.hidden : undefined,
|
|
212
|
+
installed: false,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
111
216
|
export function listMarketplace(): { recipes: MarketplaceEntry[]; pipelines: MarketplaceEntry[] } {
|
|
112
|
-
const
|
|
217
|
+
const sources = listSources();
|
|
113
218
|
const installedRecipes = listRecipes();
|
|
114
219
|
const installedWorkflows = listWorkflows();
|
|
115
220
|
|
|
221
|
+
// Recipes
|
|
116
222
|
const recipeMap = new Map<string, MarketplaceEntry>();
|
|
117
|
-
for (const
|
|
118
|
-
|
|
119
|
-
recipeMap.set(r.name, {
|
|
120
|
-
kind: 'recipe',
|
|
121
|
-
name: r.name,
|
|
122
|
-
display_name: r.display_name || r.name,
|
|
123
|
-
description: r.description,
|
|
124
|
-
version: r.version || '0.0.0',
|
|
125
|
-
author: r.author,
|
|
126
|
-
tags: r.tags,
|
|
127
|
-
score: r.score ?? 0,
|
|
128
|
-
rating: r.rating ?? 0,
|
|
129
|
-
source: 'registry',
|
|
130
|
-
installed: false,
|
|
131
|
-
});
|
|
223
|
+
for (const [name, pe] of mergeAcrossSources('recipes', sources)) {
|
|
224
|
+
recipeMap.set(name, toEntry('recipe', pe));
|
|
132
225
|
}
|
|
133
|
-
// Marketplace = registry catalog ONLY. Recipes / pipelines that
|
|
134
|
-
// exist only locally (uploaded copy, hand-edited file, custom
|
|
135
|
-
// workflow) belong to the user and don't show here — Marketplace
|
|
136
|
-
// is for browsing the remote forge-workflow repo. We still mark
|
|
137
|
-
// registry items as `installed` when a local copy exists, so the
|
|
138
|
-
// UI can show "your local copy is on v0.2.0, registry is v0.3.0".
|
|
139
226
|
for (const ir of installedRecipes) {
|
|
140
227
|
const cached = recipeMap.get(ir.name);
|
|
141
228
|
if (cached) {
|
|
@@ -143,31 +230,14 @@ export function listMarketplace(): { recipes: MarketplaceEntry[]; pipelines: Mar
|
|
|
143
230
|
cached.installed_version = ir.version;
|
|
144
231
|
cached.has_update = compareVersions(cached.version, ir.version) > 0;
|
|
145
232
|
}
|
|
146
|
-
//
|
|
233
|
+
// local-only recipe — intentionally skipped (Marketplace = remote catalog)
|
|
147
234
|
}
|
|
148
235
|
|
|
236
|
+
// Pipelines
|
|
149
237
|
const pipelineMap = new Map<string, MarketplaceEntry>();
|
|
150
|
-
for (const
|
|
151
|
-
|
|
152
|
-
pipelineMap.set(p.name, {
|
|
153
|
-
kind: 'pipeline',
|
|
154
|
-
name: p.name,
|
|
155
|
-
display_name: p.display_name || p.name,
|
|
156
|
-
description: p.description,
|
|
157
|
-
version: p.version || '0.0.0',
|
|
158
|
-
author: p.author,
|
|
159
|
-
tags: p.tags,
|
|
160
|
-
score: p.score ?? 0,
|
|
161
|
-
rating: p.rating ?? 0,
|
|
162
|
-
source: 'registry',
|
|
163
|
-
installed: false,
|
|
164
|
-
});
|
|
238
|
+
for (const [name, pe] of mergeAcrossSources('pipelines', sources)) {
|
|
239
|
+
pipelineMap.set(name, toEntry('pipeline', pe));
|
|
165
240
|
}
|
|
166
|
-
// Same model for pipelines: Marketplace lists only registry items.
|
|
167
|
-
// Local-only workflows (yours, edited, custom) live in the
|
|
168
|
-
// Pipelines tab list — they're shown there, not here. This split
|
|
169
|
-
// keeps "browse upstream catalog" and "manage your local files"
|
|
170
|
-
// as two distinct mental modes.
|
|
171
241
|
for (const iw of installedWorkflows) {
|
|
172
242
|
if (iw.builtin) continue;
|
|
173
243
|
const cached = pipelineMap.get(iw.name);
|
|
@@ -175,7 +245,7 @@ export function listMarketplace(): { recipes: MarketplaceEntry[]; pipelines: Mar
|
|
|
175
245
|
cached.installed = true;
|
|
176
246
|
cached.installed_version = (iw as any).version || cached.version;
|
|
177
247
|
}
|
|
178
|
-
//
|
|
248
|
+
// local-only pipeline — intentionally skipped
|
|
179
249
|
}
|
|
180
250
|
|
|
181
251
|
return {
|
|
@@ -186,53 +256,50 @@ export function listMarketplace(): { recipes: MarketplaceEntry[]; pipelines: Mar
|
|
|
186
256
|
|
|
187
257
|
// ─── Install ───────────────────────────────────────────────
|
|
188
258
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
259
|
+
/** Find the highest-priority source that lists `name` for `kind`. */
|
|
260
|
+
function findEntrySource(kind: WorkflowKind, name: string): SourceMeta | null {
|
|
261
|
+
const kindKey = kind === 'recipe' ? 'recipes' : 'pipelines';
|
|
262
|
+
for (const source of listSources()) {
|
|
263
|
+
const data = readSourceData(source);
|
|
264
|
+
const items = (data?.[kindKey] || []) as Array<{ name?: string }>;
|
|
265
|
+
if (items.some(i => i?.name === name)) return source;
|
|
266
|
+
}
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** Try a few candidate paths inside a source — `<kind>/<name>/<kind>.yaml`
|
|
271
|
+
* first (canonical), then the flat `<kind>/<name>.yaml` fallback. */
|
|
272
|
+
async function fetchWorkflowYaml(source: SourceMeta, kind: WorkflowKind, name: string): Promise<string | null> {
|
|
273
|
+
const subdir = kind === 'recipe' ? 'recipes' : 'pipelines';
|
|
274
|
+
const file = kind === 'recipe' ? 'recipe.yaml' : 'pipeline.yaml';
|
|
275
|
+
const candidates = [`${subdir}/${name}/${file}`, `${subdir}/${name}.yaml`];
|
|
276
|
+
for (const path of candidates) {
|
|
277
|
+
try {
|
|
278
|
+
const text = await fetchSourceFile(source, path);
|
|
279
|
+
if (text) return text;
|
|
280
|
+
} catch {
|
|
281
|
+
// try next candidate
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return null;
|
|
204
285
|
}
|
|
205
286
|
|
|
206
287
|
export async function installFromMarketplace(
|
|
207
288
|
kind: WorkflowKind,
|
|
208
289
|
name: string,
|
|
209
290
|
opts: { target_name?: string; overwrite?: boolean } = {},
|
|
210
|
-
): Promise<{ ok: boolean; error?: string; installed_as?: string }> {
|
|
211
|
-
const base = baseUrl();
|
|
212
|
-
if (!base) return { ok: false, error: 'workflowRepoUrl is empty' };
|
|
291
|
+
): Promise<{ ok: boolean; error?: string; installed_as?: string; source_id?: string }> {
|
|
213
292
|
if (!/^[a-z0-9][a-z0-9_-]*$/.test(name)) return { ok: false, error: 'invalid name' };
|
|
214
|
-
|
|
215
293
|
const targetName = opts.target_name || name;
|
|
216
294
|
if (!/^[a-z0-9][a-z0-9_-]*$/.test(targetName)) {
|
|
217
295
|
return { ok: false, error: `target_name "${targetName}" must be lowercase alphanumerics + hyphens/underscores` };
|
|
218
296
|
}
|
|
219
297
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
const candidates = [
|
|
226
|
-
`${base}/${subdir}/${name}/${file}`,
|
|
227
|
-
`${base}/${subdir}/${name}.yaml`,
|
|
228
|
-
];
|
|
229
|
-
|
|
230
|
-
let yamlText: string | null = null;
|
|
231
|
-
for (const url of candidates) {
|
|
232
|
-
yamlText = await fetchText(url);
|
|
233
|
-
if (yamlText) break;
|
|
234
|
-
}
|
|
235
|
-
if (!yamlText) return { ok: false, error: `${kind} "${name}" not found in registry` };
|
|
298
|
+
const source = findEntrySource(kind, name);
|
|
299
|
+
if (!source) return { ok: false, error: `${kind} "${name}" not found in registry — try Sync` };
|
|
300
|
+
|
|
301
|
+
const yamlText = await fetchWorkflowYaml(source, kind, name);
|
|
302
|
+
if (!yamlText) return { ok: false, error: `${kind} "${name}" file not reachable in ${source.id}` };
|
|
236
303
|
|
|
237
304
|
// Validate it parses + has the right shape before writing.
|
|
238
305
|
let parsed: any;
|
|
@@ -255,16 +322,16 @@ export async function installFromMarketplace(
|
|
|
255
322
|
return { ok: false, error: `"${targetName}" already exists locally — pass overwrite=true to replace it, or choose a different name` };
|
|
256
323
|
}
|
|
257
324
|
|
|
258
|
-
//
|
|
259
|
-
//
|
|
260
|
-
//
|
|
325
|
+
// Rename rewrites the yaml's top-level `name:` so the on-disk copy
|
|
326
|
+
// matches its filename. Without this the file would be loaded under
|
|
327
|
+
// the original name and collide with other clones.
|
|
261
328
|
let outputYaml = yamlText;
|
|
262
329
|
if (targetName !== name) {
|
|
263
330
|
parsed.name = targetName;
|
|
264
331
|
outputYaml = YAML.stringify(parsed);
|
|
265
332
|
}
|
|
266
333
|
writeFileSync(targetPath, outputYaml, { mode: 0o600 });
|
|
267
|
-
return { ok: true, installed_as: targetName };
|
|
334
|
+
return { ok: true, installed_as: targetName, source_id: source.id };
|
|
268
335
|
}
|
|
269
336
|
|
|
270
337
|
// ─── Uninstall ─────────────────────────────────────────────
|
|
@@ -274,7 +341,6 @@ export function uninstallFromMarketplace(kind: WorkflowKind, name: string): { ok
|
|
|
274
341
|
const dir = kind === 'recipe' ? join(getDataDir(), 'recipes') : join(getDataDir(), 'flows');
|
|
275
342
|
const yamlPath = join(dir, `${name}.yaml`);
|
|
276
343
|
const ymlPath = join(dir, `${name}.yml`);
|
|
277
|
-
const { unlinkSync } = require('node:fs');
|
|
278
344
|
if (existsSync(yamlPath)) { unlinkSync(yamlPath); return { ok: true }; }
|
|
279
345
|
if (existsSync(ymlPath)) { unlinkSync(ymlPath); return { ok: true }; }
|
|
280
346
|
return { ok: false, error: 'not installed' };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { NextResponse, type NextRequest } from 'next/server';
|
|
2
2
|
|
|
3
|
-
export function
|
|
3
|
+
export function proxy(req: NextRequest) {
|
|
4
4
|
// Skip auth entirely in dev mode
|
|
5
5
|
const isDev = process.env.NODE_ENV !== 'production' || process.env.FORGE_DEV === '1';
|
|
6
6
|
if (isDev) {
|
|
@@ -15,6 +15,7 @@ export function middleware(req: NextRequest) {
|
|
|
15
15
|
pathname.startsWith('/login') ||
|
|
16
16
|
pathname.startsWith('/api/auth') ||
|
|
17
17
|
pathname === '/api/version' ||
|
|
18
|
+
pathname === '/api/bridge-info' ||
|
|
18
19
|
pathname.startsWith('/api/telegram') ||
|
|
19
20
|
(pathname.startsWith('/api/workspace') && (pathname.endsWith('/smith') || pathname === '/api/workspace')) ||
|
|
20
21
|
pathname.startsWith('/_next') ||
|
package/src/core/db/database.ts
CHANGED
|
@@ -20,10 +20,16 @@ export function getDb(dbPath: string): Database.Database {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
function initSchema(db: Database.Database) {
|
|
23
|
-
// Migrations for existing tables
|
|
23
|
+
// Migrations for existing tables — silence both
|
|
24
|
+
// "duplicate column" (column already added on a prior run) AND
|
|
25
|
+
// "no such table" (fresh DB — table doesn't exist yet, will be
|
|
26
|
+
// CREATEd in the block below; ALTER is irrelevant).
|
|
27
|
+
// Anything else (typo in SQL, real corruption) still logs as an error.
|
|
24
28
|
const migrate = (sql: string) => {
|
|
25
29
|
try { db.exec(sql); } catch (e: any) {
|
|
26
|
-
|
|
30
|
+
const msg = String(e.message);
|
|
31
|
+
if (msg.includes('duplicate column') || msg.includes('no such table')) return;
|
|
32
|
+
console.error('[db] Migration failed:', sql, e.message);
|
|
27
33
|
}
|
|
28
34
|
};
|
|
29
35
|
migrate('ALTER TABLE tasks ADD COLUMN scheduled_at TEXT');
|
|
@@ -83,13 +83,6 @@
|
|
|
83
83
|
"enabled": true
|
|
84
84
|
},
|
|
85
85
|
|
|
86
|
-
"jenkins": {
|
|
87
|
-
"config": {
|
|
88
|
-
"instances": "[{\"name\":\"default-jenkins\",\"base_url\":\"http://nac-dev-jenkins.fortinet-us.com:8080\",\"username\":\"${jenkins_username}\",\"api_token\":\"${jenkins_api_token}\",\"gitlab_pat\":\"${gitlab_pat}\",\"gitlab_token_name\":\"${gitlab_token_name}\",\"gitlab_token_name_param\":\"${gitlab_token_name}\",\"gitlab_pat_param\":\"\",\"inject_params\":\"[{\\\"name\\\":\\\"TOKEN_USER\\\",\\\"value\\\":\\\"${gitlab_token_name}\\\"},{\\\"name\\\":\\\"TOKEN_PASSWORD\\\",\\\"value\\\":\\\"${gitlab_pat}\\\"}]\"}]"
|
|
89
|
-
},
|
|
90
|
-
"enabled": true
|
|
91
|
-
},
|
|
92
|
-
|
|
93
86
|
"tp": {
|
|
94
87
|
"config": {
|
|
95
88
|
"base_url": "https://nac-tp.fortinet-us.com",
|