forgeos 0.1.0-alpha.34 → 0.1.0-alpha.36
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/AGENTS.md +1 -1
- package/CHANGELOG.md +22 -0
- package/docs/changelog.md +29 -0
- package/package.json +1 -1
- package/src/forge/_generated/releaseManifest.json +1 -1
- package/src/forge/_generated/releaseManifest.ts +3 -3
- package/src/forge/cli/authmd.ts +35 -12
- package/src/forge/cli/dev.ts +12 -2
- package/src/forge/compiler/agent-contract/build.ts +46 -18
- package/src/forge/compiler/client-sdk/render-client.ts +93 -17
- package/src/forge/compiler/data-graph/sql/ddl.ts +52 -18
- package/src/forge/compiler/package-upgrades/planner.ts +134 -14
- package/src/forge/compiler/package-upgrades/types.ts +2 -0
- package/src/forge/dev/server.ts +16 -1
- package/src/forge/react/index.ts +141 -0
- package/src/forge/version.ts +1 -1
package/AGENTS.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// @forge-generated generator=0.1.0-alpha.
|
|
1
|
+
// @forge-generated generator=0.1.0-alpha.36 input=fed0c5202e23edeb9d4d7f31d0de594194b8a898741da3c1a74b8c2d54758aa5 content=721818a6f9a664aa898092d4aa5cd68cbbd8ad6e150c7d3ad3ef785a2e3fcf53
|
|
2
2
|
# AGENTS.md
|
|
3
3
|
|
|
4
4
|
<!-- forge-generated:start -->
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# forgeos
|
|
2
2
|
|
|
3
|
+
## 0.1.0-alpha.36
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Smooth the alpha field-test loop after the Vendor Access app exercise.
|
|
8
|
+
|
|
9
|
+
- Teach `forge deps upgrade-plan` to resolve npm alias dependencies such as `forge: npm:forgeos@alpha`, while preserving alias install specs like `forge@npm:forgeos@0.1.0-alpha.36` in generated upgrade plans.
|
|
10
|
+
- Normalize `auth.md` runtime risk rendering so Commands and Risk/Approval metadata agree that commands default to `write`.
|
|
11
|
+
- Add non-throwing command proof APIs through generated `client.commandResult(...)` and React `useCommandResult(...)`, so expected policy denials can be rendered as structured results instead of noisy unhandled browser failures.
|
|
12
|
+
- Update the basic example schema/docs to use `organizations` as the root tenant table instead of a self-referential `tenants` root.
|
|
13
|
+
|
|
14
|
+
## 0.1.0-alpha.35
|
|
15
|
+
|
|
16
|
+
### Patch Changes
|
|
17
|
+
|
|
18
|
+
- Harden field-test regressions found while building WorkOS-style multi-tenant apps.
|
|
19
|
+
|
|
20
|
+
- Resolve camelCase `ref:` targets such as `ref:accessRequests` to canonical SQL table names like `access_requests`, and report unknown refs before invalid SQL reaches PGlite.
|
|
21
|
+
- Allow WorkOS-like local dev auth headers through dev-server CORS, including organization, membership, permissions, roles, and claims headers.
|
|
22
|
+
- Teach the generated agent contract and capability map to recognize camelCase `ctx.db` aliases for snake_case tables.
|
|
23
|
+
- Avoid printing fake `http://127.0.0.1:0` API URLs in `forge dev --port 0` startup diagnostics.
|
|
24
|
+
|
|
3
25
|
## 0.1.0-alpha.34
|
|
4
26
|
|
|
5
27
|
### Patch Changes
|
package/docs/changelog.md
CHANGED
|
@@ -6,6 +6,35 @@ The canonical source file in the repository is `CHANGELOG.md`.
|
|
|
6
6
|
|
|
7
7
|
## Unreleased
|
|
8
8
|
|
|
9
|
+
## 0.1.0-alpha.36
|
|
10
|
+
|
|
11
|
+
- Smoothed the alpha field-test loop after the Vendor Access app exercise.
|
|
12
|
+
- Taught `forge deps upgrade-plan` to resolve npm alias dependencies such as
|
|
13
|
+
`forge: npm:forgeos@alpha`, while preserving alias install specs like
|
|
14
|
+
`forge@npm:forgeos@0.1.0-alpha.36` in generated upgrade plans.
|
|
15
|
+
- Normalized `auth.md` runtime risk rendering so Commands and Risk/Approval
|
|
16
|
+
metadata agree that commands default to `write`.
|
|
17
|
+
- Added non-throwing command proof APIs through generated
|
|
18
|
+
`client.commandResult(...)` and React `useCommandResult(...)`, so expected
|
|
19
|
+
policy denials can be rendered as structured results instead of noisy
|
|
20
|
+
unhandled browser failures.
|
|
21
|
+
- Updated the basic example schema/docs to use `organizations` as the root
|
|
22
|
+
tenant table instead of a self-referential `tenants` root.
|
|
23
|
+
|
|
24
|
+
## 0.1.0-alpha.35
|
|
25
|
+
|
|
26
|
+
- Hardened field-test regressions found while building WorkOS-style
|
|
27
|
+
multi-tenant apps.
|
|
28
|
+
- Resolved camelCase `ref:` targets such as `ref:accessRequests` to canonical
|
|
29
|
+
SQL table names like `access_requests`, and reported unknown refs before
|
|
30
|
+
invalid SQL reaches PGlite.
|
|
31
|
+
- Allowed WorkOS-like local dev auth headers through dev-server CORS,
|
|
32
|
+
including organization, membership, permissions, roles, and claims headers.
|
|
33
|
+
- Taught the generated agent contract and capability map to recognize
|
|
34
|
+
camelCase `ctx.db` aliases for snake_case tables.
|
|
35
|
+
- Avoided printing fake `http://127.0.0.1:0` API URLs in
|
|
36
|
+
`forge dev --port 0` startup diagnostics.
|
|
37
|
+
|
|
9
38
|
## 0.1.0-alpha.34
|
|
10
39
|
|
|
11
40
|
- Hardened the ForgeOS app-building DX after the Vendor Access field test.
|
package/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"defaultProvider":"local","diagnostics":[],"env":{"deployEnv":"FORGE_DEPLOY_ENV","deployId":"FORGE_DEPLOY_ID","publicReleaseId":"NEXT_PUBLIC_FORGE_RELEASE_ID","releaseId":"FORGE_RELEASE_ID"},"gitSha":"unknown","optionalProviders":["local","sentry-compatible","sentry","glitchtip","bugsink","otel","custom"],"packageName":"forgeos","packageVersion":"0.1.0-alpha.
|
|
1
|
+
{"defaultProvider":"local","diagnostics":[],"env":{"deployEnv":"FORGE_DEPLOY_ENV","deployId":"FORGE_DEPLOY_ID","publicReleaseId":"NEXT_PUBLIC_FORGE_RELEASE_ID","releaseId":"FORGE_RELEASE_ID"},"gitSha":"unknown","optionalProviders":["local","sentry-compatible","sentry","glitchtip","bugsink","otel","custom"],"packageName":"forgeos","packageVersion":"0.1.0-alpha.36","releaseId":"forgeos@0.1.0-alpha.36+unknown","schemaVersion":"0.1.0"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// @forge-generated generator=0.1.0-alpha.
|
|
1
|
+
// @forge-generated generator=0.1.0-alpha.36 input=fed0c5202e23edeb9d4d7f31d0de594194b8a898741da3c1a74b8c2d54758aa5 content=55e1e8090cce3a057473a21fe3382f8f159581327fa98ecee1a489f8de6965d0
|
|
2
2
|
export const releaseManifest = {
|
|
3
3
|
"defaultProvider": "local",
|
|
4
4
|
"diagnostics": [],
|
|
@@ -19,7 +19,7 @@ export const releaseManifest = {
|
|
|
19
19
|
"custom"
|
|
20
20
|
],
|
|
21
21
|
"packageName": "forgeos",
|
|
22
|
-
"packageVersion": "0.1.0-alpha.
|
|
23
|
-
"releaseId": "forgeos@0.1.0-alpha.
|
|
22
|
+
"packageVersion": "0.1.0-alpha.36",
|
|
23
|
+
"releaseId": "forgeos@0.1.0-alpha.36+unknown",
|
|
24
24
|
"schemaVersion": "0.1.0"
|
|
25
25
|
} as const;
|
package/src/forge/cli/authmd.ts
CHANGED
|
@@ -64,6 +64,8 @@ interface AgentToolsLike {
|
|
|
64
64
|
}>;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
type RuntimeEntryKind = "command" | "query" | "liveQuery" | "action";
|
|
68
|
+
|
|
67
69
|
function readGeneratedJson<T>(workspaceRoot: string, relativePath: string): T | null {
|
|
68
70
|
const absolute = join(workspaceRoot, relativePath);
|
|
69
71
|
if (!nodeFileSystem.exists(absolute)) {
|
|
@@ -76,11 +78,32 @@ function outputPath(options: AuthMdCommandOptions): string {
|
|
|
76
78
|
return options.output?.trim() || "public/auth.md";
|
|
77
79
|
}
|
|
78
80
|
|
|
79
|
-
function
|
|
81
|
+
function runtimeEntryRisk(kind: RuntimeEntryKind, entry: RuntimeEntry): string {
|
|
82
|
+
if (entry.risk) {
|
|
83
|
+
return entry.risk;
|
|
84
|
+
}
|
|
85
|
+
if (kind === "command") {
|
|
86
|
+
return "write";
|
|
87
|
+
}
|
|
88
|
+
if (kind === "action") {
|
|
89
|
+
return "external";
|
|
90
|
+
}
|
|
91
|
+
return "read";
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function runtimeEntryNeedsApproval(kind: RuntimeEntryKind, entry: RuntimeEntry): boolean {
|
|
95
|
+
return entry.needsApproval ?? kind === "action";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function listRuntimeEntries(
|
|
99
|
+
title: string,
|
|
100
|
+
kind: RuntimeEntryKind,
|
|
101
|
+
entries: RuntimeEntry[] | undefined,
|
|
102
|
+
): string[] {
|
|
80
103
|
const rows = (entries ?? []).map((entry) =>
|
|
81
104
|
`| \`${entry.name}\` | ${entry.policy ? `\`${entry.policy}\`` : "none"} | ${
|
|
82
105
|
entry.requiresAuth === false ? "no" : "yes"
|
|
83
|
-
} | ${entry
|
|
106
|
+
} | ${runtimeEntryRisk(kind, entry)} | ${String(runtimeEntryNeedsApproval(kind, entry))} |`
|
|
84
107
|
);
|
|
85
108
|
return [
|
|
86
109
|
`## ${title}`,
|
|
@@ -115,16 +138,16 @@ function existingDocLinks(workspaceRoot: string): string[] {
|
|
|
115
138
|
|
|
116
139
|
function riskMetadata(contract: AgentContractLike | null, tools: AgentToolsLike | null) {
|
|
117
140
|
const runtime = [
|
|
118
|
-
...(contract?.commands ?? []).map((entry) => ({ kind: "command", ...entry
|
|
119
|
-
...(contract?.queries ?? []).map((entry) => ({ kind: "query", ...entry
|
|
120
|
-
...(contract?.liveQueries ?? []).map((entry) => ({ kind: "liveQuery", ...entry
|
|
121
|
-
...(contract?.actions ?? []).map((entry) => ({ kind: "action", ...entry
|
|
141
|
+
...(contract?.commands ?? []).map((entry) => ({ kind: "command" as const, ...entry })),
|
|
142
|
+
...(contract?.queries ?? []).map((entry) => ({ kind: "query" as const, ...entry })),
|
|
143
|
+
...(contract?.liveQueries ?? []).map((entry) => ({ kind: "liveQuery" as const, ...entry })),
|
|
144
|
+
...(contract?.actions ?? []).map((entry) => ({ kind: "action" as const, ...entry })),
|
|
122
145
|
].map((entry) => ({
|
|
123
146
|
kind: entry.kind,
|
|
124
147
|
name: entry.name,
|
|
125
148
|
policy: entry.policy ?? null,
|
|
126
|
-
risk: entry.
|
|
127
|
-
needs_approval:
|
|
149
|
+
risk: runtimeEntryRisk(entry.kind, entry),
|
|
150
|
+
needs_approval: runtimeEntryNeedsApproval(entry.kind, entry),
|
|
128
151
|
requires_auth: entry.requiresAuth !== false,
|
|
129
152
|
}));
|
|
130
153
|
const aiTools = (tools?.tools ?? []).map((tool) => ({
|
|
@@ -232,10 +255,10 @@ function renderAuthMd(workspaceRoot: string): AuthMdCommandResult & { content: s
|
|
|
232
255
|
`- Roles: \`${auth?.claims?.roles ?? "roles"}\``,
|
|
233
256
|
`- Permissions: \`${auth?.claims?.permissions ?? "permissions"}\``,
|
|
234
257
|
"",
|
|
235
|
-
...listRuntimeEntries("Commands", contract?.commands),
|
|
236
|
-
...listRuntimeEntries("Queries", contract?.queries),
|
|
237
|
-
...listRuntimeEntries("Live Queries", contract?.liveQueries),
|
|
238
|
-
...listRuntimeEntries("Actions", contract?.actions),
|
|
258
|
+
...listRuntimeEntries("Commands", "command", contract?.commands),
|
|
259
|
+
...listRuntimeEntries("Queries", "query", contract?.queries),
|
|
260
|
+
...listRuntimeEntries("Live Queries", "liveQuery", contract?.liveQueries),
|
|
261
|
+
...listRuntimeEntries("Actions", "action", contract?.actions),
|
|
239
262
|
"## Policies",
|
|
240
263
|
"",
|
|
241
264
|
"| Policy | Kind | Roles | Permissions |",
|
package/src/forge/cli/dev.ts
CHANGED
|
@@ -595,6 +595,17 @@ export async function resolveAvailableWebPort(input: {
|
|
|
595
595
|
};
|
|
596
596
|
}
|
|
597
597
|
|
|
598
|
+
export function devStartupConsoleUrlOptions(input: {
|
|
599
|
+
host: string;
|
|
600
|
+
port: number;
|
|
601
|
+
webUrl?: string;
|
|
602
|
+
}): { apiUrl?: string; webUrl?: string } {
|
|
603
|
+
return {
|
|
604
|
+
...(input.port === 0 ? {} : { apiUrl: `http://${input.host}:${input.port}` }),
|
|
605
|
+
...(input.webUrl ? { webUrl: input.webUrl } : {}),
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
|
|
598
609
|
export interface DevWatchGenerateFailureEvent {
|
|
599
610
|
schemaVersion: "0.1.0";
|
|
600
611
|
event: "dev.generate_failed";
|
|
@@ -1138,8 +1149,7 @@ export async function runDevCommand(
|
|
|
1138
1149
|
mode: "startup",
|
|
1139
1150
|
strictSecrets: false,
|
|
1140
1151
|
includeImpact: true,
|
|
1141
|
-
|
|
1142
|
-
...(webUrl ? { webUrl } : {}),
|
|
1152
|
+
...devStartupConsoleUrlOptions({ host, port, ...(webUrl ? { webUrl } : {}) }),
|
|
1143
1153
|
});
|
|
1144
1154
|
if (options.json) {
|
|
1145
1155
|
process.stdout.write(formatDevConsoleJson(startupCycle));
|
|
@@ -283,9 +283,33 @@ function escapeRegExp(value: string): string {
|
|
|
283
283
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
284
284
|
}
|
|
285
285
|
|
|
286
|
+
function toSnakeCaseName(name: string): string {
|
|
287
|
+
return name
|
|
288
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1_$2")
|
|
289
|
+
.replace(/[-\s]+/g, "_")
|
|
290
|
+
.toLowerCase();
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function toCamelCaseName(name: string): string {
|
|
294
|
+
return name.replace(/[_-]([a-zA-Z0-9])/g, (_match, char: string) => char.toUpperCase());
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function dbTableLookup(dataGraph: AgentContractInput["dataGraph"]): Map<string, string> {
|
|
298
|
+
const lookup = new Map<string, string>();
|
|
299
|
+
for (const table of dataGraph.tables) {
|
|
300
|
+
lookup.set(table.name, table.name);
|
|
301
|
+
lookup.set(table.exportName, table.name);
|
|
302
|
+
lookup.set(toSnakeCaseName(table.name), table.name);
|
|
303
|
+
lookup.set(toSnakeCaseName(table.exportName), table.name);
|
|
304
|
+
lookup.set(toCamelCaseName(table.name), table.name);
|
|
305
|
+
lookup.set(toCamelCaseName(table.exportName), table.name);
|
|
306
|
+
}
|
|
307
|
+
return lookup;
|
|
308
|
+
}
|
|
309
|
+
|
|
286
310
|
function addDbAliasesForText(
|
|
287
311
|
text: string,
|
|
288
|
-
|
|
312
|
+
tableLookup: Map<string, string>,
|
|
289
313
|
aliases: Map<string, string>,
|
|
290
314
|
): void {
|
|
291
315
|
for (const match of text.matchAll(
|
|
@@ -293,8 +317,9 @@ function addDbAliasesForText(
|
|
|
293
317
|
)) {
|
|
294
318
|
const alias = match[1] ?? "";
|
|
295
319
|
const table = match[2] ?? match[3] ?? "";
|
|
296
|
-
|
|
297
|
-
|
|
320
|
+
const canonicalTable = tableLookup.get(table);
|
|
321
|
+
if (alias && canonicalTable) {
|
|
322
|
+
aliases.set(alias, canonicalTable);
|
|
298
323
|
}
|
|
299
324
|
}
|
|
300
325
|
|
|
@@ -304,8 +329,9 @@ function addDbAliasesForText(
|
|
|
304
329
|
const [rawTable, rawAlias] = part.split(":").map((value) => value.trim());
|
|
305
330
|
const table = rawTable?.replace(/["'`]/g, "") ?? "";
|
|
306
331
|
const alias = (rawAlias ?? rawTable ?? "").replace(/\s*=.*$/, "").trim();
|
|
307
|
-
|
|
308
|
-
|
|
332
|
+
const canonicalTable = tableLookup.get(table);
|
|
333
|
+
if (canonicalTable && alias) {
|
|
334
|
+
aliases.set(alias, canonicalTable);
|
|
309
335
|
}
|
|
310
336
|
}
|
|
311
337
|
}
|
|
@@ -313,26 +339,28 @@ function addDbAliasesForText(
|
|
|
313
339
|
|
|
314
340
|
function dbTablesForText(
|
|
315
341
|
text: string,
|
|
316
|
-
|
|
342
|
+
tableLookup: Map<string, string>,
|
|
317
343
|
ops: Set<string>,
|
|
318
344
|
): string[] {
|
|
319
345
|
const tables: string[] = [];
|
|
320
346
|
for (const match of text.matchAll(/ctx\.db\.([A-Za-z_$][A-Za-z0-9_$]*)\s*\.\s*([A-Za-z_$][A-Za-z0-9_$]*)/g)) {
|
|
321
347
|
const table = match[1] ?? "";
|
|
322
348
|
const op = match[2] ?? "";
|
|
323
|
-
|
|
324
|
-
|
|
349
|
+
const canonicalTable = tableLookup.get(table);
|
|
350
|
+
if (canonicalTable && ops.has(op)) {
|
|
351
|
+
tables.push(canonicalTable);
|
|
325
352
|
}
|
|
326
353
|
}
|
|
327
354
|
for (const match of text.matchAll(/ctx\.db\s*\[\s*["'`]([^"'`]+)["'`]\s*\]\s*\.\s*([A-Za-z_$][A-Za-z0-9_$]*)/g)) {
|
|
328
355
|
const table = match[1] ?? "";
|
|
329
356
|
const op = match[2] ?? "";
|
|
330
|
-
|
|
331
|
-
|
|
357
|
+
const canonicalTable = tableLookup.get(table);
|
|
358
|
+
if (canonicalTable && ops.has(op)) {
|
|
359
|
+
tables.push(canonicalTable);
|
|
332
360
|
}
|
|
333
361
|
}
|
|
334
362
|
const aliases = new Map<string, string>();
|
|
335
|
-
addDbAliasesForText(text,
|
|
363
|
+
addDbAliasesForText(text, tableLookup, aliases);
|
|
336
364
|
for (const [alias, table] of aliases) {
|
|
337
365
|
const aliasPattern = new RegExp(`\\b${escapeRegExp(alias)}\\s*\\.\\s*([A-Za-z_$][A-Za-z0-9_$]*)`, "g");
|
|
338
366
|
for (const match of text.matchAll(aliasPattern)) {
|
|
@@ -348,10 +376,10 @@ function dbTablesForText(
|
|
|
348
376
|
function dbTablesForFile(
|
|
349
377
|
workspaceRoot: string,
|
|
350
378
|
file: string | undefined,
|
|
351
|
-
|
|
379
|
+
tableLookup: Map<string, string>,
|
|
352
380
|
ops: Set<string>,
|
|
353
381
|
): string[] {
|
|
354
|
-
return dbTablesForText(sourceTextWithLocalImports(workspaceRoot, file),
|
|
382
|
+
return dbTablesForText(sourceTextWithLocalImports(workspaceRoot, file), tableLookup, ops);
|
|
355
383
|
}
|
|
356
384
|
|
|
357
385
|
function emittedEventsForFile(workspaceRoot: string, file: string | undefined): string[] {
|
|
@@ -1222,7 +1250,7 @@ export function buildAgentContractArtifacts(
|
|
|
1222
1250
|
);
|
|
1223
1251
|
|
|
1224
1252
|
const runtimeEntries = new Map(input.runtimeGraph.entries.map((entry) => [entry.name, entry]));
|
|
1225
|
-
const
|
|
1253
|
+
const tableLookup = dbTableLookup(input.dataGraph);
|
|
1226
1254
|
const externalEntries = input.externalServices?.services.flatMap((service) => service.entries) ?? [];
|
|
1227
1255
|
const localCommandInfos: AgentContract["commands"] = sorted(Object.keys(input.apiSurface.commands), (name) => name).map((name) => {
|
|
1228
1256
|
const entry = runtimeEntries.get(name);
|
|
@@ -1232,8 +1260,8 @@ export function buildAgentContractArtifacts(
|
|
|
1232
1260
|
file,
|
|
1233
1261
|
source: "local" as const,
|
|
1234
1262
|
policy: authPolicy(commandAuth.get(name)),
|
|
1235
|
-
tablesRead: dbTablesForFile(input.workspaceRoot, file,
|
|
1236
|
-
tablesWritten: dbTablesForFile(input.workspaceRoot, file,
|
|
1263
|
+
tablesRead: dbTablesForFile(input.workspaceRoot, file, tableLookup, DB_READ_OPS),
|
|
1264
|
+
tablesWritten: dbTablesForFile(input.workspaceRoot, file, tableLookup, DB_WRITE_OPS),
|
|
1237
1265
|
emits: emittedEventsForFile(input.workspaceRoot, file),
|
|
1238
1266
|
allowedPackages: entry ? packageNamesForModule(input.appGraph, entry.moduleId) : [],
|
|
1239
1267
|
forbiddenCapabilities: forbiddenForContext(input.classified, "command"),
|
|
@@ -1268,7 +1296,7 @@ export function buildAgentContractArtifacts(
|
|
|
1268
1296
|
policy: authPolicy(queryAuth.get(query.name)),
|
|
1269
1297
|
readOnly: true,
|
|
1270
1298
|
tenantScoped: input.tenantScope.tables.length > 0,
|
|
1271
|
-
tablesRead: dbTablesForFile(input.workspaceRoot, query.file,
|
|
1299
|
+
tablesRead: dbTablesForFile(input.workspaceRoot, query.file, tableLookup, DB_READ_OPS),
|
|
1272
1300
|
allowedPackages: packageNamesForModule(input.appGraph, query.moduleId),
|
|
1273
1301
|
forbiddenCapabilities: forbiddenForContext(input.classified, "query"),
|
|
1274
1302
|
http: httpEndpointFor("query", query.name),
|
|
@@ -1296,7 +1324,7 @@ export function buildAgentContractArtifacts(
|
|
|
1296
1324
|
);
|
|
1297
1325
|
const liveQueryInfos: AgentContract["liveQueries"] = sorted(input.liveQueryRegistry.liveQueries, (liveQuery) => liveQuery.name).map(
|
|
1298
1326
|
(liveQuery) => {
|
|
1299
|
-
const tablesRead = dbTablesForFile(input.workspaceRoot, liveQuery.file,
|
|
1327
|
+
const tablesRead = dbTablesForFile(input.workspaceRoot, liveQuery.file, tableLookup, DB_READ_OPS);
|
|
1300
1328
|
return {
|
|
1301
1329
|
name: liveQuery.name,
|
|
1302
1330
|
file: liveQuery.file,
|
|
@@ -86,6 +86,22 @@ export type LiveSnapshot<T> = {
|
|
|
86
86
|
traceId?: string;
|
|
87
87
|
};
|
|
88
88
|
|
|
89
|
+
export type ForgeCommandResult<T = unknown> =
|
|
90
|
+
| {
|
|
91
|
+
ok: true;
|
|
92
|
+
result: T;
|
|
93
|
+
status: number;
|
|
94
|
+
traceId?: string;
|
|
95
|
+
diagnostics?: { code: string; message: string }[];
|
|
96
|
+
}
|
|
97
|
+
| {
|
|
98
|
+
ok: false;
|
|
99
|
+
error: { code: string; message: string; details?: unknown };
|
|
100
|
+
status: number;
|
|
101
|
+
traceId?: string;
|
|
102
|
+
diagnostics?: { code: string; message: string }[];
|
|
103
|
+
};
|
|
104
|
+
|
|
89
105
|
export type LiveQueryOptions = {
|
|
90
106
|
signal?: AbortSignal;
|
|
91
107
|
};
|
|
@@ -96,6 +112,7 @@ export type ForgeClient = {
|
|
|
96
112
|
readonly lastTraceId?: string;
|
|
97
113
|
query<Name extends QueryName>(name: Name, args: unknown): Promise<unknown>;
|
|
98
114
|
command<Name extends CommandName>(name: Name, args: unknown): Promise<unknown>;
|
|
115
|
+
commandResult<Name extends CommandName>(name: Name, args: unknown): Promise<ForgeCommandResult<unknown>>;
|
|
99
116
|
externalQuery<Name extends ExternalQueryRef>(name: Name, args: unknown): Promise<unknown>;
|
|
100
117
|
externalCommand<Name extends ExternalCommandRef>(name: Name, args: unknown): Promise<unknown>;
|
|
101
118
|
liveQuery<Name extends LiveQueryName>(
|
|
@@ -113,6 +130,7 @@ export function renderClientTs(): string {
|
|
|
113
130
|
return `import { api } from "./api.ts";
|
|
114
131
|
import type {
|
|
115
132
|
ForgeAuthProvider,
|
|
133
|
+
ForgeCommandResult,
|
|
116
134
|
ForgeClient,
|
|
117
135
|
ForgeClientConfig,
|
|
118
136
|
ExternalCommandRef,
|
|
@@ -128,6 +146,7 @@ export type {
|
|
|
128
146
|
ForgeAuthProvider,
|
|
129
147
|
ForgeClient,
|
|
130
148
|
ForgeClientConfig,
|
|
149
|
+
ForgeCommandResult,
|
|
131
150
|
ForgeStaticAuth,
|
|
132
151
|
QueryName,
|
|
133
152
|
CommandName,
|
|
@@ -258,6 +277,10 @@ class ForgeHttpClient implements ForgeClient {
|
|
|
258
277
|
return this.invoke("commands", name, args);
|
|
259
278
|
}
|
|
260
279
|
|
|
280
|
+
commandResult(name: string, args: unknown): Promise<ForgeCommandResult<unknown>> {
|
|
281
|
+
return this.invokeResult("commands", name, args);
|
|
282
|
+
}
|
|
283
|
+
|
|
261
284
|
externalQuery(name: ExternalQueryRef, args: unknown): Promise<unknown> {
|
|
262
285
|
return this.invokeExternal("queries", name, args);
|
|
263
286
|
}
|
|
@@ -297,27 +320,61 @@ class ForgeHttpClient implements ForgeClient {
|
|
|
297
320
|
name: string,
|
|
298
321
|
args: unknown,
|
|
299
322
|
): Promise<unknown> {
|
|
323
|
+
const result = await this.invokeResult(kind, name, args);
|
|
324
|
+
if (!result.ok) {
|
|
325
|
+
throw new ForgeError(result.error.message, {
|
|
326
|
+
code: result.error.code,
|
|
327
|
+
traceId: result.traceId,
|
|
328
|
+
status: result.status,
|
|
329
|
+
details: result.error.details ?? result.diagnostics,
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
return result.result;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
private async invokeResult(
|
|
336
|
+
kind: "queries" | "commands",
|
|
337
|
+
name: string,
|
|
338
|
+
args: unknown,
|
|
339
|
+
): Promise<ForgeCommandResult<unknown>> {
|
|
300
340
|
const baseUrl = this.config.url.replace(/\\/$/, "");
|
|
301
341
|
const url = \`\${baseUrl}/\${kind}/\${encodeURIComponent(name)}\`;
|
|
302
342
|
const authHeaders = await resolveAuthHeaders(this.config.auth);
|
|
303
343
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
344
|
+
let response: Response;
|
|
345
|
+
try {
|
|
346
|
+
response = await fetch(url, {
|
|
347
|
+
method: "POST",
|
|
348
|
+
headers: {
|
|
349
|
+
"Content-Type": "application/json",
|
|
350
|
+
...authHeaders,
|
|
351
|
+
},
|
|
352
|
+
body: JSON.stringify({ args }),
|
|
353
|
+
});
|
|
354
|
+
} catch (error) {
|
|
355
|
+
return {
|
|
356
|
+
ok: false,
|
|
357
|
+
status: 0,
|
|
358
|
+
error: {
|
|
359
|
+
code: "FORGE_NETWORK_ERROR",
|
|
360
|
+
message: error instanceof Error ? error.message : "network request failed",
|
|
361
|
+
details: error,
|
|
362
|
+
},
|
|
363
|
+
};
|
|
364
|
+
}
|
|
312
365
|
|
|
313
366
|
let body: unknown;
|
|
314
367
|
try {
|
|
315
368
|
body = await response.json();
|
|
316
369
|
} catch {
|
|
317
|
-
|
|
318
|
-
|
|
370
|
+
return {
|
|
371
|
+
ok: false,
|
|
319
372
|
status: response.status,
|
|
320
|
-
|
|
373
|
+
error: {
|
|
374
|
+
code: "FORGE_HTTP_ERROR",
|
|
375
|
+
message: \`HTTP \${response.status}\`,
|
|
376
|
+
},
|
|
377
|
+
};
|
|
321
378
|
}
|
|
322
379
|
|
|
323
380
|
const payload = parseJsonPayload(body);
|
|
@@ -331,15 +388,26 @@ class ForgeHttpClient implements ForgeClient {
|
|
|
331
388
|
payload.error?.message ??
|
|
332
389
|
diagnostic?.message ??
|
|
333
390
|
\`Request failed with status \${response.status}\`;
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
traceId: payload.traceId,
|
|
391
|
+
return {
|
|
392
|
+
ok: false,
|
|
337
393
|
status: response.status,
|
|
338
|
-
|
|
339
|
-
|
|
394
|
+
traceId: payload.traceId,
|
|
395
|
+
diagnostics: payload.diagnostics,
|
|
396
|
+
error: {
|
|
397
|
+
code,
|
|
398
|
+
message,
|
|
399
|
+
details: payload.error?.details ?? payload.diagnostics,
|
|
400
|
+
},
|
|
401
|
+
};
|
|
340
402
|
}
|
|
341
403
|
|
|
342
|
-
return
|
|
404
|
+
return {
|
|
405
|
+
ok: true,
|
|
406
|
+
result: payload.result,
|
|
407
|
+
status: response.status,
|
|
408
|
+
traceId: payload.traceId,
|
|
409
|
+
diagnostics: payload.diagnostics,
|
|
410
|
+
};
|
|
343
411
|
}
|
|
344
412
|
|
|
345
413
|
private async invokeExternal(
|
|
@@ -524,8 +592,11 @@ export type {
|
|
|
524
592
|
ForgeReactAuthProvider,
|
|
525
593
|
ForgeReactClient,
|
|
526
594
|
ForgeReactError,
|
|
595
|
+
ForgeCommandCallResult,
|
|
527
596
|
UseCommandOptions,
|
|
528
597
|
UseCommandResult,
|
|
598
|
+
UseCommandResultHook,
|
|
599
|
+
UseCommandResultOptions,
|
|
529
600
|
UseLiveQueryOptions,
|
|
530
601
|
UseLiveQueryResult,
|
|
531
602
|
UseQueryOptions,
|
|
@@ -539,6 +610,7 @@ export const useForgeClient = forgeReact.useForgeClient;
|
|
|
539
610
|
export const useAuth = forgeReact.useAuth;
|
|
540
611
|
export const useQuery = forgeReact.useQuery;
|
|
541
612
|
export const useCommand = forgeReact.useCommand;
|
|
613
|
+
export const useCommandResult = forgeReact.useCommandResult;
|
|
542
614
|
export const useLiveQuery = forgeReact.useLiveQuery;
|
|
543
615
|
`;
|
|
544
616
|
}
|
|
@@ -551,8 +623,11 @@ export function renderReactDts(): string {
|
|
|
551
623
|
ForgeReactAuthProvider,
|
|
552
624
|
ForgeReactClient,
|
|
553
625
|
ForgeReactError,
|
|
626
|
+
ForgeCommandCallResult,
|
|
554
627
|
UseCommandOptions,
|
|
555
628
|
UseCommandResult,
|
|
629
|
+
UseCommandResultHook,
|
|
630
|
+
UseCommandResultOptions,
|
|
556
631
|
UseLiveQueryOptions,
|
|
557
632
|
UseLiveQueryResult,
|
|
558
633
|
UseQueryOptions,
|
|
@@ -564,6 +639,7 @@ export declare const useForgeClient: import("forge/react").ForgeReactBindings["u
|
|
|
564
639
|
export declare const useAuth: import("forge/react").ForgeReactBindings["useAuth"];
|
|
565
640
|
export declare const useQuery: import("forge/react").ForgeReactBindings["useQuery"];
|
|
566
641
|
export declare const useCommand: import("forge/react").ForgeReactBindings["useCommand"];
|
|
642
|
+
export declare const useCommandResult: import("forge/react").ForgeReactBindings["useCommandResult"];
|
|
567
643
|
export declare const useLiveQuery: import("forge/react").ForgeReactBindings["useLiveQuery"];
|
|
568
644
|
`;
|
|
569
645
|
}
|
|
@@ -102,39 +102,41 @@ function parseFieldType(
|
|
|
102
102
|
field: DataField,
|
|
103
103
|
tableName: string,
|
|
104
104
|
diagnostics: Diagnostic[],
|
|
105
|
+
refTables: Map<string, string>,
|
|
105
106
|
): ParsedFieldType | null {
|
|
106
107
|
const declared = field.type.trim();
|
|
107
108
|
const nullable = declared.endsWith("?");
|
|
108
|
-
const raw = (nullable ? declared.slice(0, -1) : declared).trim()
|
|
109
|
+
const raw = (nullable ? declared.slice(0, -1) : declared).trim();
|
|
110
|
+
const lowered = raw.toLowerCase();
|
|
109
111
|
|
|
110
112
|
const withNullability = (parsed: Omit<ParsedFieldType, "nullable">): ParsedFieldType => ({
|
|
111
113
|
...parsed,
|
|
112
114
|
...(nullable ? { nullable: true } : {}),
|
|
113
115
|
});
|
|
114
116
|
|
|
115
|
-
if (
|
|
117
|
+
if (lowered === "uuid") {
|
|
116
118
|
return withNullability({ sqlType: "uuid" });
|
|
117
119
|
}
|
|
118
|
-
if (
|
|
120
|
+
if (lowered === "text" || lowered === "string") {
|
|
119
121
|
return withNullability({ sqlType: "text" });
|
|
120
122
|
}
|
|
121
|
-
if (
|
|
123
|
+
if (lowered === "number") {
|
|
122
124
|
return withNullability({ sqlType: "double precision" });
|
|
123
125
|
}
|
|
124
|
-
if (
|
|
126
|
+
if (lowered === "integer" || lowered === "int") {
|
|
125
127
|
return withNullability({ sqlType: "integer" });
|
|
126
128
|
}
|
|
127
|
-
if (
|
|
129
|
+
if (lowered === "boolean" || lowered === "bool") {
|
|
128
130
|
return withNullability({ sqlType: "boolean" });
|
|
129
131
|
}
|
|
130
|
-
if (
|
|
132
|
+
if (lowered === "timestamp" || lowered === "timestamptz") {
|
|
131
133
|
return withNullability({ sqlType: "timestamptz" });
|
|
132
134
|
}
|
|
133
|
-
if (
|
|
135
|
+
if (lowered === "json") {
|
|
134
136
|
return withNullability({ sqlType: "jsonb" });
|
|
135
137
|
}
|
|
136
138
|
|
|
137
|
-
if (
|
|
139
|
+
if (lowered.startsWith("enum:")) {
|
|
138
140
|
const values = raw
|
|
139
141
|
.slice("enum:".length)
|
|
140
142
|
.split(",")
|
|
@@ -158,11 +160,11 @@ function parseFieldType(
|
|
|
158
160
|
};
|
|
159
161
|
}
|
|
160
162
|
|
|
161
|
-
if (
|
|
163
|
+
if (lowered.startsWith("enum_")) {
|
|
162
164
|
return withNullability({ sqlType: "text" });
|
|
163
165
|
}
|
|
164
166
|
|
|
165
|
-
if (
|
|
167
|
+
if (lowered.startsWith("ref:")) {
|
|
166
168
|
const refTable = raw.slice("ref:".length).trim();
|
|
167
169
|
if (!refTable) {
|
|
168
170
|
diagnostics.push(
|
|
@@ -174,14 +176,25 @@ function parseFieldType(
|
|
|
174
176
|
);
|
|
175
177
|
return null;
|
|
176
178
|
}
|
|
179
|
+
const resolvedRefTable = refTables.get(refTable) ?? refTables.get(toSnakeCase(refTable));
|
|
180
|
+
if (!resolvedRefTable) {
|
|
181
|
+
diagnostics.push(
|
|
182
|
+
createDiagnostic({
|
|
183
|
+
severity: "error",
|
|
184
|
+
code: FORGE_DB_INVALID_SQL_PLAN,
|
|
185
|
+
message: `unknown ref target '${refTable}' on ${tableName}.${field.name}; expected one of ${[...new Set(refTables.values())].sort().join(", ")}`,
|
|
186
|
+
}),
|
|
187
|
+
);
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
177
190
|
return {
|
|
178
191
|
...(nullable ? { nullable: true } : {}),
|
|
179
192
|
sqlType: "uuid",
|
|
180
|
-
references: { table:
|
|
193
|
+
references: { table: resolvedRefTable, column: "id" },
|
|
181
194
|
};
|
|
182
195
|
}
|
|
183
196
|
|
|
184
|
-
if (
|
|
197
|
+
if (lowered === "ref") {
|
|
185
198
|
return {
|
|
186
199
|
...(nullable ? { nullable: true } : {}),
|
|
187
200
|
sqlType: "uuid",
|
|
@@ -199,6 +212,18 @@ function parseFieldType(
|
|
|
199
212
|
return null;
|
|
200
213
|
}
|
|
201
214
|
|
|
215
|
+
function buildReferenceTableLookup(tables: DataTable[]): Map<string, string> {
|
|
216
|
+
const lookup = new Map<string, string>();
|
|
217
|
+
for (const table of tables) {
|
|
218
|
+
const sqlTableName = toSnakeCase(table.name);
|
|
219
|
+
lookup.set(table.name, sqlTableName);
|
|
220
|
+
lookup.set(table.exportName, sqlTableName);
|
|
221
|
+
lookup.set(sqlTableName, sqlTableName);
|
|
222
|
+
lookup.set(toSnakeCase(table.exportName), sqlTableName);
|
|
223
|
+
}
|
|
224
|
+
return lookup;
|
|
225
|
+
}
|
|
226
|
+
|
|
202
227
|
function defaultExprForField(field: DataField): string | undefined {
|
|
203
228
|
const name = field.name;
|
|
204
229
|
const type = field.type.trim().replace(/\?$/, "").toLowerCase();
|
|
@@ -217,7 +242,11 @@ function defaultExprForField(field: DataField): string | undefined {
|
|
|
217
242
|
return undefined;
|
|
218
243
|
}
|
|
219
244
|
|
|
220
|
-
function buildColumns(
|
|
245
|
+
function buildColumns(
|
|
246
|
+
table: DataTable,
|
|
247
|
+
diagnostics: Diagnostic[],
|
|
248
|
+
refTables: Map<string, string>,
|
|
249
|
+
): ColumnDef[] {
|
|
221
250
|
const columns: ColumnDef[] = [];
|
|
222
251
|
const fields = [...table.fields].sort((a, b) =>
|
|
223
252
|
a.name < b.name ? -1 : a.name > b.name ? 1 : 0,
|
|
@@ -236,7 +265,7 @@ function buildColumns(table: DataTable, diagnostics: Diagnostic[]): ColumnDef[]
|
|
|
236
265
|
}
|
|
237
266
|
|
|
238
267
|
for (const field of fields) {
|
|
239
|
-
const parsed = parseFieldType(field, table.name, diagnostics);
|
|
268
|
+
const parsed = parseFieldType(field, table.name, diagnostics, refTables);
|
|
240
269
|
if (!parsed) {
|
|
241
270
|
continue;
|
|
242
271
|
}
|
|
@@ -311,9 +340,13 @@ function buildForeignKeyIndexes(tableName: string, columns: ColumnDef[]): IndexD
|
|
|
311
340
|
return indexes;
|
|
312
341
|
}
|
|
313
342
|
|
|
314
|
-
function buildTableChange(
|
|
343
|
+
function buildTableChange(
|
|
344
|
+
table: DataTable,
|
|
345
|
+
diagnostics: Diagnostic[],
|
|
346
|
+
refTables: Map<string, string>,
|
|
347
|
+
): SqlChange | null {
|
|
315
348
|
const tableName = toSnakeCase(table.name);
|
|
316
|
-
const columns = buildColumns(table, diagnostics);
|
|
349
|
+
const columns = buildColumns(table, diagnostics, refTables);
|
|
317
350
|
|
|
318
351
|
if (columns.length === 0) {
|
|
319
352
|
diagnostics.push(
|
|
@@ -351,6 +384,7 @@ export function buildSqlPlan(dataGraph: DataGraph): SqlPlan {
|
|
|
351
384
|
const diagnostics: Diagnostic[] = [];
|
|
352
385
|
const tables: SqlChange[] = [];
|
|
353
386
|
const indexes: SqlChange[] = [];
|
|
387
|
+
const refTables = buildReferenceTableLookup(dataGraph.tables);
|
|
354
388
|
|
|
355
389
|
const systemTables: SqlChange[] = [
|
|
356
390
|
{
|
|
@@ -406,7 +440,7 @@ export function buildSqlPlan(dataGraph: DataGraph): SqlPlan {
|
|
|
406
440
|
];
|
|
407
441
|
|
|
408
442
|
for (const table of stableSortTables(dataGraph.tables)) {
|
|
409
|
-
const change = buildTableChange(table, diagnostics);
|
|
443
|
+
const change = buildTableChange(table, diagnostics, refTables);
|
|
410
444
|
if (!change) {
|
|
411
445
|
continue;
|
|
412
446
|
}
|
|
@@ -102,7 +102,62 @@ function readPackageJson(workspaceRoot: string): {
|
|
|
102
102
|
};
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
|
|
105
|
+
type DependencyType = "dependency" | "devDependency";
|
|
106
|
+
|
|
107
|
+
interface NpmAliasSpec {
|
|
108
|
+
packageName: string;
|
|
109
|
+
versionOrRange?: string;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
interface PackageResolution {
|
|
113
|
+
requestedName: string;
|
|
114
|
+
packageName: string;
|
|
115
|
+
dependencyName: string;
|
|
116
|
+
dependencySpec?: string;
|
|
117
|
+
dependencyType: DependencyType;
|
|
118
|
+
isNpmAlias: boolean;
|
|
119
|
+
current: PackageApi;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function parseNpmAliasSpec(spec: string): NpmAliasSpec | null {
|
|
123
|
+
if (!spec.startsWith("npm:")) {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
const target = spec.slice("npm:".length);
|
|
127
|
+
if (!target) {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
const separator =
|
|
131
|
+
target.startsWith("@")
|
|
132
|
+
? target.indexOf("@", target.indexOf("/") + 1)
|
|
133
|
+
: target.indexOf("@");
|
|
134
|
+
if (separator === -1) {
|
|
135
|
+
return { packageName: target };
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
packageName: target.slice(0, separator),
|
|
139
|
+
versionOrRange: target.slice(separator + 1),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function dependencyEntries(
|
|
144
|
+
pkg: ReturnType<typeof readPackageJson>,
|
|
145
|
+
): Array<{ type: DependencyType; name: string; spec: string }> {
|
|
146
|
+
return [
|
|
147
|
+
...Object.entries(pkg.dependencies ?? {}).map(([name, spec]) => ({
|
|
148
|
+
type: "dependency" as const,
|
|
149
|
+
name,
|
|
150
|
+
spec,
|
|
151
|
+
})),
|
|
152
|
+
...Object.entries(pkg.devDependencies ?? {}).map(([name, spec]) => ({
|
|
153
|
+
type: "devDependency" as const,
|
|
154
|
+
name,
|
|
155
|
+
spec,
|
|
156
|
+
})),
|
|
157
|
+
];
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function dependencyType(workspaceRoot: string, packageName: string): DependencyType {
|
|
106
161
|
const pkg = readPackageJson(workspaceRoot);
|
|
107
162
|
if (packageName in (pkg.devDependencies ?? {})) {
|
|
108
163
|
return "devDependency";
|
|
@@ -110,9 +165,59 @@ function dependencyType(workspaceRoot: string, packageName: string): "dependency
|
|
|
110
165
|
return "dependency";
|
|
111
166
|
}
|
|
112
167
|
|
|
113
|
-
function
|
|
168
|
+
function resolvePackageForUpgrade(workspaceRoot: string, requestedName: string): PackageResolution | null {
|
|
114
169
|
const graph = readGeneratedJson<PackageGraph>(workspaceRoot, `${GENERATED_DIR}/packageGraph.json`);
|
|
115
|
-
|
|
170
|
+
if (!graph) {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
const pkg = readPackageJson(workspaceRoot);
|
|
174
|
+
const entries = dependencyEntries(pkg);
|
|
175
|
+
|
|
176
|
+
const aliasEntry = entries.find((entry) => {
|
|
177
|
+
const alias = parseNpmAliasSpec(entry.spec);
|
|
178
|
+
return entry.name === requestedName || alias?.packageName === requestedName;
|
|
179
|
+
});
|
|
180
|
+
if (aliasEntry) {
|
|
181
|
+
const alias = parseNpmAliasSpec(aliasEntry.spec);
|
|
182
|
+
const packageName = alias?.packageName ?? aliasEntry.name;
|
|
183
|
+
const current = graph.packages.find((candidate) => candidate.name === packageName);
|
|
184
|
+
if (current) {
|
|
185
|
+
return {
|
|
186
|
+
requestedName,
|
|
187
|
+
packageName,
|
|
188
|
+
dependencyName: aliasEntry.name,
|
|
189
|
+
dependencySpec: aliasEntry.spec,
|
|
190
|
+
dependencyType: aliasEntry.type,
|
|
191
|
+
isNpmAlias: Boolean(alias),
|
|
192
|
+
current,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const current = graph.packages.find((candidate) => candidate.name === requestedName);
|
|
198
|
+
if (!current) {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
const directEntry = entries.find((entry) => entry.name === requestedName);
|
|
202
|
+
return {
|
|
203
|
+
requestedName,
|
|
204
|
+
packageName: requestedName,
|
|
205
|
+
dependencyName: requestedName,
|
|
206
|
+
dependencySpec: directEntry?.spec,
|
|
207
|
+
dependencyType: directEntry?.type ?? dependencyType(workspaceRoot, requestedName),
|
|
208
|
+
isNpmAlias: false,
|
|
209
|
+
current,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function packageSpecForDependency(
|
|
214
|
+
resolution: Pick<PackageResolution, "packageName" | "dependencyName" | "isNpmAlias">,
|
|
215
|
+
version: string,
|
|
216
|
+
): string {
|
|
217
|
+
if (resolution.isNpmAlias) {
|
|
218
|
+
return `${resolution.dependencyName}@npm:${resolution.packageName}@${version}`;
|
|
219
|
+
}
|
|
220
|
+
return `${resolution.packageName}@${version}`;
|
|
116
221
|
}
|
|
117
222
|
|
|
118
223
|
function fixtureMetadata(registryDir: string | undefined, packageName: string): {
|
|
@@ -348,9 +453,9 @@ export async function createUpgradePlan(
|
|
|
348
453
|
): Promise<UpgradePlannerResult> {
|
|
349
454
|
const diagnostics: Diagnostic[] = [];
|
|
350
455
|
const packageManager = detectPackageManager(options.workspaceRoot);
|
|
351
|
-
const
|
|
456
|
+
const resolution = resolvePackageForUpgrade(options.workspaceRoot, options.packageName);
|
|
352
457
|
|
|
353
|
-
if (!
|
|
458
|
+
if (!resolution) {
|
|
354
459
|
return {
|
|
355
460
|
ok: false,
|
|
356
461
|
diagnostics: [
|
|
@@ -363,10 +468,11 @@ export async function createUpgradePlan(
|
|
|
363
468
|
exitCode: 1,
|
|
364
469
|
};
|
|
365
470
|
}
|
|
471
|
+
const current = resolution.current;
|
|
366
472
|
|
|
367
473
|
const targetInfo = await resolveTargetVersion({
|
|
368
474
|
workspaceRoot: options.workspaceRoot,
|
|
369
|
-
packageName:
|
|
475
|
+
packageName: resolution.packageName,
|
|
370
476
|
currentVersion: current.version,
|
|
371
477
|
target: options.target,
|
|
372
478
|
registryDir: options.registryDir ?? process.env.FORGE_DEPS_REGISTRY_DIR,
|
|
@@ -388,7 +494,7 @@ export async function createUpgradePlan(
|
|
|
388
494
|
|
|
389
495
|
const analyzed = await analyzeTargetPackage({
|
|
390
496
|
workspaceRoot: options.workspaceRoot,
|
|
391
|
-
packageName:
|
|
497
|
+
packageName: resolution.packageName,
|
|
392
498
|
versionInfo: targetInfo,
|
|
393
499
|
packageManager,
|
|
394
500
|
registryDir: options.registryDir ?? process.env.FORGE_DEPS_REGISTRY_DIR,
|
|
@@ -398,7 +504,7 @@ export async function createUpgradePlan(
|
|
|
398
504
|
return { ok: false, diagnostics, exitCode: 1 };
|
|
399
505
|
}
|
|
400
506
|
|
|
401
|
-
const recipe = resolveByPackageName(
|
|
507
|
+
const recipe = resolveByPackageName(resolution.packageName) ?? undefined;
|
|
402
508
|
const currentClassified = { api: current, classification: classify(current, recipe), recipe };
|
|
403
509
|
const targetClassified = {
|
|
404
510
|
api: analyzed.api,
|
|
@@ -425,7 +531,7 @@ export async function createUpgradePlan(
|
|
|
425
531
|
}
|
|
426
532
|
|
|
427
533
|
const affected = analyzeUpgradeImpact({
|
|
428
|
-
packageName:
|
|
534
|
+
packageName: resolution.packageName,
|
|
429
535
|
appGraph,
|
|
430
536
|
runtimeGraph,
|
|
431
537
|
queryRegistry,
|
|
@@ -436,21 +542,35 @@ export async function createUpgradePlan(
|
|
|
436
542
|
const bump = semverBump(current.version, analyzed.api.version);
|
|
437
543
|
const risk = buildRiskReport({ bump, apiDiff, runtimeDiff, affected });
|
|
438
544
|
const commands = recommendedCommands(affected);
|
|
545
|
+
const toInfo = {
|
|
546
|
+
...targetInfo,
|
|
547
|
+
version: analyzed.api.version,
|
|
548
|
+
spec: packageSpecForDependency(resolution, analyzed.api.version),
|
|
549
|
+
};
|
|
439
550
|
const planDir = packagePlanDir(
|
|
440
551
|
options.workspaceRoot,
|
|
441
|
-
|
|
552
|
+
resolution.dependencyName,
|
|
442
553
|
current.version,
|
|
443
554
|
analyzed.api.version,
|
|
444
555
|
);
|
|
445
|
-
const id = `${packageKey(
|
|
556
|
+
const id = `${packageKey(resolution.dependencyName)}-${current.version}-to-${analyzed.api.version}`;
|
|
446
557
|
const plan: PackageUpgradePlan = {
|
|
447
558
|
schemaVersion: "0.1.0",
|
|
448
559
|
plannerVersion: GENERATOR_VERSION,
|
|
449
560
|
id,
|
|
450
|
-
packageName:
|
|
561
|
+
packageName: resolution.packageName,
|
|
562
|
+
...(resolution.requestedName !== resolution.packageName
|
|
563
|
+
? { requestedPackageName: resolution.requestedName }
|
|
564
|
+
: {}),
|
|
565
|
+
...(resolution.isNpmAlias ? { dependencyAlias: resolution.dependencyName } : {}),
|
|
451
566
|
...(recipe?.alias ? { integrationAlias: recipe.alias } : {}),
|
|
452
|
-
from: {
|
|
453
|
-
|
|
567
|
+
from: {
|
|
568
|
+
version: current.version,
|
|
569
|
+
spec: resolution.isNpmAlias
|
|
570
|
+
? packageSpecForDependency(resolution, current.version)
|
|
571
|
+
: `${resolution.packageName}@${current.version}`,
|
|
572
|
+
},
|
|
573
|
+
to: toInfo,
|
|
454
574
|
packageManager,
|
|
455
575
|
semver: {
|
|
456
576
|
bump,
|
|
@@ -146,6 +146,8 @@ export interface PackageUpgradePlan {
|
|
|
146
146
|
plannerVersion: string;
|
|
147
147
|
id: string;
|
|
148
148
|
packageName: string;
|
|
149
|
+
requestedPackageName?: string;
|
|
150
|
+
dependencyAlias?: string;
|
|
149
151
|
integrationAlias?: string;
|
|
150
152
|
from: PackageVersionInfo;
|
|
151
153
|
to: PackageVersionInfo;
|
package/src/forge/dev/server.ts
CHANGED
|
@@ -333,12 +333,27 @@ function markdownResponse(body: string, status = 200): Response {
|
|
|
333
333
|
}
|
|
334
334
|
|
|
335
335
|
function corsPreflight(): Response {
|
|
336
|
+
const allowedHeaders = [
|
|
337
|
+
"Authorization",
|
|
338
|
+
"Content-Type",
|
|
339
|
+
"WorkOS-Signature",
|
|
340
|
+
"x-forge-user-id",
|
|
341
|
+
"x-forge-tenant-id",
|
|
342
|
+
"x-forge-organization-id",
|
|
343
|
+
"x-forge-organization-membership-id",
|
|
344
|
+
"x-forge-role",
|
|
345
|
+
"x-forge-roles",
|
|
346
|
+
"x-forge-permissions",
|
|
347
|
+
"x-forge-claims",
|
|
348
|
+
"x-forge-auth-kind",
|
|
349
|
+
"x-forge-trace-id",
|
|
350
|
+
];
|
|
336
351
|
return new Response(null, {
|
|
337
352
|
status: 204,
|
|
338
353
|
headers: {
|
|
339
354
|
"Access-Control-Allow-Origin": "*",
|
|
340
355
|
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
341
|
-
"Access-Control-Allow-Headers": "
|
|
356
|
+
"Access-Control-Allow-Headers": allowedHeaders.join(", "),
|
|
342
357
|
},
|
|
343
358
|
});
|
|
344
359
|
}
|
package/src/forge/react/index.ts
CHANGED
|
@@ -45,6 +45,22 @@ export type ForgeReactError = Error & {
|
|
|
45
45
|
details?: unknown;
|
|
46
46
|
};
|
|
47
47
|
|
|
48
|
+
export type ForgeCommandCallResult<T = unknown> =
|
|
49
|
+
| {
|
|
50
|
+
ok: true;
|
|
51
|
+
result: T;
|
|
52
|
+
status: number;
|
|
53
|
+
traceId?: string;
|
|
54
|
+
diagnostics?: { code: string; message: string }[];
|
|
55
|
+
}
|
|
56
|
+
| {
|
|
57
|
+
ok: false;
|
|
58
|
+
error: { code: string; message: string; details?: unknown };
|
|
59
|
+
status: number;
|
|
60
|
+
traceId?: string;
|
|
61
|
+
diagnostics?: { code: string; message: string }[];
|
|
62
|
+
};
|
|
63
|
+
|
|
48
64
|
export type LiveSnapshot<T> = {
|
|
49
65
|
subscriptionId: string;
|
|
50
66
|
revision: number;
|
|
@@ -56,6 +72,7 @@ export type ForgeReactClient = {
|
|
|
56
72
|
lastTraceId?: string;
|
|
57
73
|
query(name: string, args: unknown): Promise<unknown>;
|
|
58
74
|
command(name: string, args: unknown): Promise<unknown>;
|
|
75
|
+
commandResult?(name: string, args: unknown): Promise<ForgeCommandCallResult<unknown>>;
|
|
59
76
|
liveQuery(
|
|
60
77
|
name: string,
|
|
61
78
|
args: unknown,
|
|
@@ -96,6 +113,22 @@ export type UseCommandResult<TArgs, TResult> = {
|
|
|
96
113
|
reset: () => void;
|
|
97
114
|
};
|
|
98
115
|
|
|
116
|
+
export type UseCommandResultOptions<TResult> = {
|
|
117
|
+
onSuccess?: (result: TResult) => void;
|
|
118
|
+
onError?: (error: ForgeReactError) => void;
|
|
119
|
+
onSettled?: (result: ForgeCommandCallResult<TResult>) => void;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
export type UseCommandResultHook<TArgs, TResult> = {
|
|
123
|
+
run: (args: TArgs) => Promise<ForgeCommandCallResult<TResult>>;
|
|
124
|
+
loading: boolean;
|
|
125
|
+
error: ForgeReactError | null;
|
|
126
|
+
result: ForgeCommandCallResult<TResult> | undefined;
|
|
127
|
+
data: TResult | undefined;
|
|
128
|
+
traceId?: string;
|
|
129
|
+
reset: () => void;
|
|
130
|
+
};
|
|
131
|
+
|
|
99
132
|
export type UseLiveQueryOptions = {
|
|
100
133
|
enabled?: boolean;
|
|
101
134
|
};
|
|
@@ -123,6 +156,10 @@ export type ForgeReactBindings<TClient extends ForgeReactClient = ForgeReactClie
|
|
|
123
156
|
name: string,
|
|
124
157
|
options?: UseCommandOptions<TResult>,
|
|
125
158
|
) => UseCommandResult<TArgs, TResult>;
|
|
159
|
+
useCommandResult: <TArgs = unknown, TResult = unknown>(
|
|
160
|
+
name: string,
|
|
161
|
+
options?: UseCommandResultOptions<TResult>,
|
|
162
|
+
) => UseCommandResultHook<TArgs, TResult>;
|
|
126
163
|
useLiveQuery: <TResult = unknown>(
|
|
127
164
|
name: string,
|
|
128
165
|
args: unknown,
|
|
@@ -165,6 +202,29 @@ function toForgeError(error: unknown): ForgeReactError {
|
|
|
165
202
|
return wrapped;
|
|
166
203
|
}
|
|
167
204
|
|
|
205
|
+
function commandErrorToForgeError(result: Extract<ForgeCommandCallResult, { ok: false }>): ForgeReactError {
|
|
206
|
+
const error = new Error(result.error.message) as ForgeReactError;
|
|
207
|
+
error.code = result.error.code;
|
|
208
|
+
error.status = result.status;
|
|
209
|
+
error.traceId = result.traceId;
|
|
210
|
+
error.details = result.error.details ?? result.diagnostics;
|
|
211
|
+
return error;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function caughtErrorToCommandResult(error: unknown): ForgeCommandCallResult<never> {
|
|
215
|
+
const forgeError = toForgeError(error);
|
|
216
|
+
return {
|
|
217
|
+
ok: false,
|
|
218
|
+
status: forgeError.status ?? 0,
|
|
219
|
+
traceId: forgeError.traceId,
|
|
220
|
+
error: {
|
|
221
|
+
code: forgeError.code ?? "FORGE_REACT_COMMAND_FAILED",
|
|
222
|
+
message: forgeError.message,
|
|
223
|
+
details: forgeError.details,
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
168
228
|
function resolveDevAuth(devAuth: ForgeDevAuthConfig | undefined): ForgeReactAuthProvider | undefined {
|
|
169
229
|
if (!devAuth) {
|
|
170
230
|
return undefined;
|
|
@@ -344,6 +404,86 @@ export function createForgeReactBindings<TClient extends ForgeReactClient>(
|
|
|
344
404
|
};
|
|
345
405
|
}
|
|
346
406
|
|
|
407
|
+
function useCommandResult<TArgs = unknown, TResult = unknown>(
|
|
408
|
+
name: string,
|
|
409
|
+
options?: UseCommandResultOptions<TResult>,
|
|
410
|
+
): UseCommandResultHook<TArgs, TResult> {
|
|
411
|
+
const client = useForgeClient();
|
|
412
|
+
const [state, setState] = React.useState<{
|
|
413
|
+
loading: boolean;
|
|
414
|
+
error: ForgeReactError | null;
|
|
415
|
+
result: ForgeCommandCallResult<TResult> | undefined;
|
|
416
|
+
data: TResult | undefined;
|
|
417
|
+
traceId?: string;
|
|
418
|
+
}>({
|
|
419
|
+
loading: false,
|
|
420
|
+
error: null,
|
|
421
|
+
result: undefined,
|
|
422
|
+
data: undefined,
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
const run = React.useCallback(
|
|
426
|
+
async (args: TArgs): Promise<ForgeCommandCallResult<TResult>> => {
|
|
427
|
+
setState((prev) => ({ ...prev, loading: true, error: null }));
|
|
428
|
+
let result: ForgeCommandCallResult<TResult>;
|
|
429
|
+
if (typeof client.commandResult === "function") {
|
|
430
|
+
result = (await client.commandResult(name, args)) as ForgeCommandCallResult<TResult>;
|
|
431
|
+
} else {
|
|
432
|
+
try {
|
|
433
|
+
const data = (await client.command(name, args)) as TResult;
|
|
434
|
+
result = {
|
|
435
|
+
ok: true,
|
|
436
|
+
result: data,
|
|
437
|
+
status: 200,
|
|
438
|
+
traceId: client.lastTraceId,
|
|
439
|
+
};
|
|
440
|
+
} catch (error) {
|
|
441
|
+
result = caughtErrorToCommandResult(error);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if (result.ok) {
|
|
446
|
+
setState({
|
|
447
|
+
loading: false,
|
|
448
|
+
error: null,
|
|
449
|
+
result,
|
|
450
|
+
data: result.result,
|
|
451
|
+
traceId: result.traceId ?? client.lastTraceId,
|
|
452
|
+
});
|
|
453
|
+
options?.onSuccess?.(result.result);
|
|
454
|
+
} else {
|
|
455
|
+
const forgeError = commandErrorToForgeError(result);
|
|
456
|
+
setState({
|
|
457
|
+
loading: false,
|
|
458
|
+
error: forgeError,
|
|
459
|
+
result,
|
|
460
|
+
data: undefined,
|
|
461
|
+
traceId: result.traceId,
|
|
462
|
+
});
|
|
463
|
+
options?.onError?.(forgeError);
|
|
464
|
+
}
|
|
465
|
+
options?.onSettled?.(result);
|
|
466
|
+
return result;
|
|
467
|
+
},
|
|
468
|
+
[client, name, options?.onError, options?.onSettled, options?.onSuccess],
|
|
469
|
+
);
|
|
470
|
+
|
|
471
|
+
const reset = React.useCallback(() => {
|
|
472
|
+
setState({
|
|
473
|
+
loading: false,
|
|
474
|
+
error: null,
|
|
475
|
+
result: undefined,
|
|
476
|
+
data: undefined,
|
|
477
|
+
});
|
|
478
|
+
}, []);
|
|
479
|
+
|
|
480
|
+
return {
|
|
481
|
+
...state,
|
|
482
|
+
run,
|
|
483
|
+
reset,
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
|
|
347
487
|
function useLiveQuery<TResult = unknown>(
|
|
348
488
|
name: string,
|
|
349
489
|
args: unknown,
|
|
@@ -430,6 +570,7 @@ export function createForgeReactBindings<TClient extends ForgeReactClient>(
|
|
|
430
570
|
useAuth,
|
|
431
571
|
useQuery,
|
|
432
572
|
useCommand,
|
|
573
|
+
useCommandResult,
|
|
433
574
|
useLiveQuery,
|
|
434
575
|
};
|
|
435
576
|
}
|
package/src/forge/version.ts
CHANGED