forgeos 0.1.0-alpha.35 → 0.1.0-alpha.37

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 CHANGED
@@ -1,4 +1,4 @@
1
- // @forge-generated generator=0.1.0-alpha.35 input=a41a4935f71e0376ad8546906f6e5bd7c01f6e033fc3aba669f636ad528e9881 content=721818a6f9a664aa898092d4aa5cd68cbbd8ad6e150c7d3ad3ef785a2e3fcf53
1
+ // @forge-generated generator=0.1.0-alpha.37 input=055c7d65fca3d6d8b05af2899c233ff69d377edf0f46f719e2d164ee0574309a 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.37
4
+
5
+ ### Patch Changes
6
+
7
+ - Fix `forge deps upgrade-plan` for npm alias installs used by ForgeOS apps.
8
+
9
+ - Record the real package name from package metadata in the generated package graph when a dependency is installed through an alias such as `forge: npm:forgeos@...`.
10
+ - Resolve upgrade plans by either the real package name (`forgeos`) or the dependency alias (`forge`) while preserving install specs like `forge@npm:forgeos@0.1.0-alpha.37`.
11
+ - Avoid attempting to resolve the unrelated public `forge@alpha` package when an app asks to upgrade its ForgeOS alias dependency.
12
+ - Add regression coverage for aliased package graph entries where the import name and real npm package name differ.
13
+
14
+ ## 0.1.0-alpha.36
15
+
16
+ ### Patch Changes
17
+
18
+ - Smooth the alpha field-test loop after the Vendor Access app exercise.
19
+
20
+ - 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.
21
+ - Normalize `auth.md` runtime risk rendering so Commands and Risk/Approval metadata agree that commands default to `write`.
22
+ - 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.
23
+ - Update the basic example schema/docs to use `organizations` as the root tenant table instead of a self-referential `tenants` root.
24
+
3
25
  ## 0.1.0-alpha.35
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.37
10
+
11
+ - Fixed `forge deps upgrade-plan` for npm alias installs used by ForgeOS apps.
12
+ - Recorded the real package name from package metadata in generated package
13
+ graph entries when a dependency is installed through an alias such as
14
+ `forge: npm:forgeos@...`.
15
+ - Resolved upgrade plans by either the real package name (`forgeos`) or the
16
+ dependency alias (`forge`) while preserving install specs like
17
+ `forge@npm:forgeos@0.1.0-alpha.37`.
18
+ - Avoided attempting to resolve the unrelated public `forge@alpha` package when
19
+ an app asks to upgrade its ForgeOS alias dependency.
20
+ - Added regression coverage for aliased package graph entries where the import
21
+ name and real npm package name differ.
22
+
23
+ ## 0.1.0-alpha.36
24
+
25
+ - Smoothed the alpha field-test loop after the Vendor Access app exercise.
26
+ - Taught `forge deps upgrade-plan` to resolve npm alias dependencies such as
27
+ `forge: npm:forgeos@alpha`, while preserving alias install specs like
28
+ `forge@npm:forgeos@0.1.0-alpha.36` in generated upgrade plans.
29
+ - Normalized `auth.md` runtime risk rendering so Commands and Risk/Approval
30
+ metadata agree that commands default to `write`.
31
+ - Added non-throwing command proof APIs through generated
32
+ `client.commandResult(...)` and React `useCommandResult(...)`, so expected
33
+ policy denials can be rendered as structured results instead of noisy
34
+ unhandled browser failures.
35
+ - Updated the basic example schema/docs to use `organizations` as the root
36
+ tenant table instead of a self-referential `tenants` root.
37
+
9
38
  ## 0.1.0-alpha.35
10
39
 
11
40
  - Hardened field-test regressions found while building WorkOS-style
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forgeos",
3
- "version": "0.1.0-alpha.35",
3
+ "version": "0.1.0-alpha.37",
4
4
  "description": "Agent-native application framework and compiler for building Forge apps without a mandatory dashboard.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -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.35","releaseId":"forgeos@0.1.0-alpha.35+unknown","schemaVersion":"0.1.0"}
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.37","releaseId":"forgeos@0.1.0-alpha.37+unknown","schemaVersion":"0.1.0"}
@@ -1,4 +1,4 @@
1
- // @forge-generated generator=0.1.0-alpha.35 input=a41a4935f71e0376ad8546906f6e5bd7c01f6e033fc3aba669f636ad528e9881 content=55c7152fe779387c1acecdd5901f3a8affd1cfc9de8ef40b1c8883e083ebfa8a
1
+ // @forge-generated generator=0.1.0-alpha.37 input=055c7d65fca3d6d8b05af2899c233ff69d377edf0f46f719e2d164ee0574309a content=1e26c5249853569091c1b295aab06bd2b9546462328fe8126eda099222430b61
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.35",
23
- "releaseId": "forgeos@0.1.0-alpha.35+unknown",
22
+ "packageVersion": "0.1.0-alpha.37",
23
+ "releaseId": "forgeos@0.1.0-alpha.37+unknown",
24
24
  "schemaVersion": "0.1.0"
25
25
  } as const;
@@ -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 listRuntimeEntries(title: string, entries: RuntimeEntry[] | undefined): string[] {
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.risk ?? "read"} | ${String(entry.needsApproval ?? false)} |`
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, risk: entry.risk ?? "write" })),
119
- ...(contract?.queries ?? []).map((entry) => ({ kind: "query", ...entry, risk: entry.risk ?? "read" })),
120
- ...(contract?.liveQueries ?? []).map((entry) => ({ kind: "liveQuery", ...entry, risk: entry.risk ?? "read" })),
121
- ...(contract?.actions ?? []).map((entry) => ({ kind: "action", ...entry, risk: entry.risk ?? "external" })),
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.risk ?? "read",
127
- needs_approval: entry.needsApproval ?? (entry.kind === "action"),
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 |",
@@ -93,7 +93,9 @@ function findPackage(workspaceRoot: string, packageName: string): {
93
93
  const lock = nodeFileSystem.exists(join(workspaceRoot, "forge.lock"))
94
94
  ? (JSON.parse((nodeFileSystem.readText(join(workspaceRoot, "forge.lock")) ?? "")) as ForgeLock)
95
95
  : null;
96
- const pkg = graph?.packages.find((candidate) => candidate.name === packageName);
96
+ const pkg = graph?.packages.find((candidate) =>
97
+ candidate.name === packageName || candidate.packageName === packageName
98
+ );
97
99
  return { graph, pkg, lock };
98
100
  }
99
101
 
@@ -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
- const response = await fetch(url, {
305
- method: "POST",
306
- headers: {
307
- "Content-Type": "application/json",
308
- ...authHeaders,
309
- },
310
- body: JSON.stringify({ args }),
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
- throw new ForgeError(\`HTTP \${response.status}\`, {
318
- code: "FORGE_HTTP_ERROR",
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
- throw new ForgeError(message, {
335
- code,
336
- traceId: payload.traceId,
391
+ return {
392
+ ok: false,
337
393
  status: response.status,
338
- details: payload.error?.details ?? payload.diagnostics,
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 payload.result;
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
  }
@@ -271,6 +271,7 @@ export class PackageGraphCompiler {
271
271
 
272
272
  const partialApi = {
273
273
  name: dep.name,
274
+ ...(packageJson.name && packageJson.name !== dep.name ? { packageName: packageJson.name } : {}),
274
275
  version: dep.version,
275
276
  packageManager: dep.packageManager,
276
277
  resolutionMode: opts.resolutionMode,
@@ -454,10 +455,11 @@ function portableDtsPath(dtsPath: string, dep: Pick<Dependency, "installPath" |
454
455
  }
455
456
 
456
457
  function readPackageJson(installPath: string): {
458
+ name?: string;
457
459
  exports?: unknown;
458
460
  } {
459
461
  const raw = readTextFile(join(installPath, "package.json"));
460
- return JSON.parse(raw) as { exports?: unknown };
462
+ return JSON.parse(raw) as { name?: string; exports?: unknown };
461
463
  }
462
464
 
463
465
  function resolveSandboxBackend(
@@ -1,4 +1,4 @@
1
1
  export const PACKAGE_GRAPH_SCHEMA_VERSION = "1.0.0";
2
- export const PACKAGE_ANALYZER_VERSION = "0.2.0";
2
+ export const PACKAGE_ANALYZER_VERSION = "0.2.1";
3
3
  export { GENERATOR_VERSION } from "../../version.ts";
4
4
  export const DEFAULT_PATTERN_EXPANSION_LIMIT = 50;
@@ -102,17 +102,138 @@ function readPackageJson(workspaceRoot: string): {
102
102
  };
103
103
  }
104
104
 
105
- function dependencyType(workspaceRoot: string, packageName: string): "dependency" | "devDependency" {
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 matchesPackageName(candidate: PackageApi, packageName: string): boolean {
123
+ return candidate.name === packageName || candidate.packageName === packageName;
124
+ }
125
+
126
+ function parseNpmAliasSpec(spec: string): NpmAliasSpec | null {
127
+ if (!spec.startsWith("npm:")) {
128
+ return null;
129
+ }
130
+ const target = spec.slice("npm:".length);
131
+ if (!target) {
132
+ return null;
133
+ }
134
+ const separator =
135
+ target.startsWith("@")
136
+ ? target.indexOf("@", target.indexOf("/") + 1)
137
+ : target.indexOf("@");
138
+ if (separator === -1) {
139
+ return { packageName: target };
140
+ }
141
+ return {
142
+ packageName: target.slice(0, separator),
143
+ versionOrRange: target.slice(separator + 1),
144
+ };
145
+ }
146
+
147
+ function dependencyEntries(
148
+ pkg: ReturnType<typeof readPackageJson>,
149
+ ): Array<{ type: DependencyType; name: string; spec: string }> {
150
+ return [
151
+ ...Object.entries(pkg.dependencies ?? {}).map(([name, spec]) => ({
152
+ type: "dependency" as const,
153
+ name,
154
+ spec,
155
+ })),
156
+ ...Object.entries(pkg.devDependencies ?? {}).map(([name, spec]) => ({
157
+ type: "devDependency" as const,
158
+ name,
159
+ spec,
160
+ })),
161
+ ];
162
+ }
163
+
164
+ function dependencyType(workspaceRoot: string, packageName: string): DependencyType {
106
165
  const pkg = readPackageJson(workspaceRoot);
107
166
  if (packageName in (pkg.devDependencies ?? {})) {
108
167
  return "devDependency";
109
168
  }
169
+ for (const entry of dependencyEntries(pkg)) {
170
+ const alias = parseNpmAliasSpec(entry.spec);
171
+ if (alias?.packageName === packageName) {
172
+ return entry.type;
173
+ }
174
+ }
110
175
  return "dependency";
111
176
  }
112
177
 
113
- function currentPackageApi(workspaceRoot: string, packageName: string): PackageApi | null {
178
+ function resolvePackageForUpgrade(workspaceRoot: string, requestedName: string): PackageResolution | null {
114
179
  const graph = readGeneratedJson<PackageGraph>(workspaceRoot, `${GENERATED_DIR}/packageGraph.json`);
115
- return graph?.packages.find((pkg) => pkg.name === packageName) ?? null;
180
+ if (!graph) {
181
+ return null;
182
+ }
183
+ const pkg = readPackageJson(workspaceRoot);
184
+ const entries = dependencyEntries(pkg);
185
+
186
+ const aliasEntry = entries.find((entry) => {
187
+ const alias = parseNpmAliasSpec(entry.spec);
188
+ return entry.name === requestedName || alias?.packageName === requestedName;
189
+ });
190
+ if (aliasEntry) {
191
+ const alias = parseNpmAliasSpec(aliasEntry.spec);
192
+ const packageName = alias?.packageName ?? aliasEntry.name;
193
+ const current = graph.packages.find((candidate) => matchesPackageName(candidate, packageName)) ??
194
+ graph.packages.find((candidate) => candidate.name === aliasEntry.name);
195
+ if (current) {
196
+ return {
197
+ requestedName,
198
+ packageName,
199
+ dependencyName: aliasEntry.name,
200
+ dependencySpec: aliasEntry.spec,
201
+ dependencyType: aliasEntry.type,
202
+ isNpmAlias: Boolean(alias),
203
+ current,
204
+ };
205
+ }
206
+ }
207
+
208
+ const current = graph.packages.find((candidate) => matchesPackageName(candidate, requestedName));
209
+ if (!current) {
210
+ return null;
211
+ }
212
+ const directEntry = entries.find((entry) => {
213
+ const alias = parseNpmAliasSpec(entry.spec);
214
+ return entry.name === requestedName || alias?.packageName === requestedName;
215
+ });
216
+ const alias = directEntry ? parseNpmAliasSpec(directEntry.spec) : null;
217
+ const packageName = alias?.packageName ?? current.packageName ?? requestedName;
218
+ return {
219
+ requestedName,
220
+ packageName,
221
+ dependencyName: directEntry?.name ?? requestedName,
222
+ dependencySpec: directEntry?.spec,
223
+ dependencyType: directEntry?.type ?? dependencyType(workspaceRoot, requestedName),
224
+ isNpmAlias: Boolean(alias),
225
+ current,
226
+ };
227
+ }
228
+
229
+ function packageSpecForDependency(
230
+ resolution: Pick<PackageResolution, "packageName" | "dependencyName" | "isNpmAlias">,
231
+ version: string,
232
+ ): string {
233
+ if (resolution.isNpmAlias) {
234
+ return `${resolution.dependencyName}@npm:${resolution.packageName}@${version}`;
235
+ }
236
+ return `${resolution.packageName}@${version}`;
116
237
  }
117
238
 
118
239
  function fixtureMetadata(registryDir: string | undefined, packageName: string): {
@@ -348,9 +469,9 @@ export async function createUpgradePlan(
348
469
  ): Promise<UpgradePlannerResult> {
349
470
  const diagnostics: Diagnostic[] = [];
350
471
  const packageManager = detectPackageManager(options.workspaceRoot);
351
- const current = currentPackageApi(options.workspaceRoot, options.packageName);
472
+ const resolution = resolvePackageForUpgrade(options.workspaceRoot, options.packageName);
352
473
 
353
- if (!current) {
474
+ if (!resolution) {
354
475
  return {
355
476
  ok: false,
356
477
  diagnostics: [
@@ -363,10 +484,11 @@ export async function createUpgradePlan(
363
484
  exitCode: 1,
364
485
  };
365
486
  }
487
+ const current = resolution.current;
366
488
 
367
489
  const targetInfo = await resolveTargetVersion({
368
490
  workspaceRoot: options.workspaceRoot,
369
- packageName: options.packageName,
491
+ packageName: resolution.packageName,
370
492
  currentVersion: current.version,
371
493
  target: options.target,
372
494
  registryDir: options.registryDir ?? process.env.FORGE_DEPS_REGISTRY_DIR,
@@ -388,7 +510,7 @@ export async function createUpgradePlan(
388
510
 
389
511
  const analyzed = await analyzeTargetPackage({
390
512
  workspaceRoot: options.workspaceRoot,
391
- packageName: options.packageName,
513
+ packageName: resolution.packageName,
392
514
  versionInfo: targetInfo,
393
515
  packageManager,
394
516
  registryDir: options.registryDir ?? process.env.FORGE_DEPS_REGISTRY_DIR,
@@ -398,7 +520,7 @@ export async function createUpgradePlan(
398
520
  return { ok: false, diagnostics, exitCode: 1 };
399
521
  }
400
522
 
401
- const recipe = resolveByPackageName(options.packageName) ?? undefined;
523
+ const recipe = resolveByPackageName(resolution.packageName) ?? undefined;
402
524
  const currentClassified = { api: current, classification: classify(current, recipe), recipe };
403
525
  const targetClassified = {
404
526
  api: analyzed.api,
@@ -425,7 +547,7 @@ export async function createUpgradePlan(
425
547
  }
426
548
 
427
549
  const affected = analyzeUpgradeImpact({
428
- packageName: options.packageName,
550
+ packageName: resolution.packageName,
429
551
  appGraph,
430
552
  runtimeGraph,
431
553
  queryRegistry,
@@ -436,21 +558,35 @@ export async function createUpgradePlan(
436
558
  const bump = semverBump(current.version, analyzed.api.version);
437
559
  const risk = buildRiskReport({ bump, apiDiff, runtimeDiff, affected });
438
560
  const commands = recommendedCommands(affected);
561
+ const toInfo = {
562
+ ...targetInfo,
563
+ version: analyzed.api.version,
564
+ spec: packageSpecForDependency(resolution, analyzed.api.version),
565
+ };
439
566
  const planDir = packagePlanDir(
440
567
  options.workspaceRoot,
441
- options.packageName,
568
+ resolution.dependencyName,
442
569
  current.version,
443
570
  analyzed.api.version,
444
571
  );
445
- const id = `${packageKey(options.packageName)}-${current.version}-to-${analyzed.api.version}`;
572
+ const id = `${packageKey(resolution.dependencyName)}-${current.version}-to-${analyzed.api.version}`;
446
573
  const plan: PackageUpgradePlan = {
447
574
  schemaVersion: "0.1.0",
448
575
  plannerVersion: GENERATOR_VERSION,
449
576
  id,
450
- packageName: options.packageName,
577
+ packageName: resolution.packageName,
578
+ ...(resolution.requestedName !== resolution.packageName
579
+ ? { requestedPackageName: resolution.requestedName }
580
+ : {}),
581
+ ...(resolution.isNpmAlias ? { dependencyAlias: resolution.dependencyName } : {}),
451
582
  ...(recipe?.alias ? { integrationAlias: recipe.alias } : {}),
452
- from: { version: current.version, spec: `${options.packageName}@${current.version}` },
453
- to: { ...targetInfo, version: analyzed.api.version },
583
+ from: {
584
+ version: current.version,
585
+ spec: resolution.isNpmAlias
586
+ ? packageSpecForDependency(resolution, current.version)
587
+ : `${resolution.packageName}@${current.version}`,
588
+ },
589
+ to: toInfo,
454
590
  packageManager,
455
591
  semver: {
456
592
  bump,
@@ -512,18 +648,19 @@ export function listOutdatedFromFixture(options: {
512
648
 
513
649
  return graph.packages
514
650
  .flatMap((pkg) => {
515
- const metadata = fixtureMetadata(registryDir, pkg.name);
651
+ const packageName = pkg.packageName ?? pkg.name;
652
+ const metadata = fixtureMetadata(registryDir, packageName);
516
653
  const latest = metadata?.["dist-tags"]?.latest;
517
654
  if (!latest || latest === pkg.version) {
518
655
  return [];
519
656
  }
520
- const recipe = resolveByPackageName(pkg.name);
657
+ const recipe = resolveByPackageName(packageName);
521
658
  return [{
522
- name: pkg.name,
659
+ name: packageName,
523
660
  current: pkg.version,
524
661
  wanted: latest,
525
662
  latest,
526
- type: dependencyType(options.workspaceRoot, pkg.name),
663
+ type: dependencyType(options.workspaceRoot, packageName),
527
664
  managedByRecipe: Boolean(recipe),
528
665
  ...(recipe?.alias ? { recipe: recipe.alias } : {}),
529
666
  }];
@@ -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;
@@ -92,7 +92,13 @@ export interface Entrypoint {
92
92
  }
93
93
 
94
94
  export interface PackageApi {
95
+ /**
96
+ * Import/dependency name used by the app, which may be an npm alias such as
97
+ * "forge" for "npm:forgeos@...".
98
+ */
95
99
  name: string;
100
+ /** Real package.json name when it differs from the dependency/import name. */
101
+ packageName?: string;
96
102
  version: string;
97
103
  packageManager: PackageManager;
98
104
  resolutionMode: ResolutionMode;
@@ -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
  }
@@ -1,3 +1,3 @@
1
- export const FORGEOS_VERSION = "0.1.0-alpha.35";
1
+ export const FORGEOS_VERSION = "0.1.0-alpha.37";
2
2
  export const GENERATOR_VERSION = FORGEOS_VERSION;
3
3
  export const CLI_VERSION = FORGEOS_VERSION;