forgeos 0.1.0-alpha.37 → 0.1.0-alpha.39

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.
@@ -17,21 +17,30 @@ function formatHelp(): string {
17
17
  " forge changed --review --json Show review-focused app/config/docs changes, excluding local agent/browser artifacts",
18
18
  " forge changed --commit-ready --json Show files suitable for git add, excluding generated and operational artifacts",
19
19
  " forge new my-app --template minimal-web --field-test Create an installed WorkOS/auth.md field-test app",
20
+ " forge field-test create vendor-access --auth workos --template minimal-web --json Create a real field-test app",
21
+ " forge field-test run --templates minimal-web,nuxt-web --package-managers npm,pnpm --runtime-probes --auth-probes --json",
22
+ " forge field-test report --json Summarize the machine-readable field-test report",
20
23
  " forge diff authored Run the authored-only git diff pathspec",
21
24
  " forge handoff --json Compact work handoff for the next external code agent",
22
25
  " forge agent onboard --target codex --json Prepare adapter, hooks, memory, and dev snapshot",
23
26
  " forge doctor agent --target codex --json Check adapter, hooks, and Agent Memory readiness",
24
27
  " forge doctor delta --json Check DeltaDB writability, queue drain, redaction, and gitignore posture",
28
+ " forge doctor runtime --json Check generated freshness, local dev lifecycle, and PGlite posture",
25
29
  " forge agent ingest codex --watch --file .forge/agent/events.ndjson --json",
26
30
  " forge docs check --json Check public docs, ReadTheDocs config, links, and local MkDocs tooling",
27
31
  " forge docs check --build --install-venv --json Build docs strictly in a local RTD-style venv",
28
- " forge release doctor --json Aggregate release, sourcemaps, self-host, and docs readiness",
32
+ " forge release doctor --json Check npm publish readiness plus separate production deploy readiness",
29
33
  " forge authmd generate Write public/auth.md from the generated agent/auth contract",
34
+ " forge auth check --production --json Fail unless auth is jwt/oidc production-ready",
30
35
  " forge authmd check --json Check public/auth.md drift for CI and agent-ready apps",
31
36
  " forge workos install --yes --json Delegate AuthKit setup to npx --yes workos@latest install",
32
37
  " forge workos doctor --json Check WorkOS AuthKit/FGA files, claims, seed, webhook, and tenant guards",
33
38
  " forge workos doctor --yes --json Run local checks, then delegate to npx --yes workos@latest doctor",
34
- " forge workos seed --file workos-seed.yml --json Plan WorkOS CLI seed",
39
+ " forge workos seed --file workos-seed.yml --dry-run --json Validate WorkOS seed without hosted changes",
40
+ " forge deploy plan --target docker --json Explain production deploy gates and commands",
41
+ " forge deploy check --production --json Gate auth, DB, metadata, generated artifacts, and liveQuery readiness",
42
+ " forge deploy render docker Write Docker production deploy files under deploy/",
43
+ " forge deploy verify --production --url https://app.example.com --json Probe /health and validate public auth metadata",
35
44
  " forge release check --allow-missing-local-release --json Gate release readiness without failing on unprepared local artifacts",
36
45
  " forge self-host check --prepared-only --json Report compose readiness without creating deploy files",
37
46
  " forge delta status --verbose --json Include Delta schema, lock, and aggregate count details",
@@ -69,6 +69,7 @@ const REQUIRED_GITIGNORE_PATHS = [
69
69
  ".forge/impact/",
70
70
  ".forge/agent-adapters/",
71
71
  ".forge/studio/",
72
+ ".workos-seed-state.json",
72
73
  ] as const;
73
74
 
74
75
  const DEFAULT_FORGE_PACKAGE_SPEC = "npm:forgeos@alpha";
@@ -570,7 +571,11 @@ export async function runNewCommand(options: NewCommandOptions): Promise<NewComm
570
571
  if (options.fieldTest && installed) {
571
572
  const steps: Array<{ name: string; command: string; args: string[] }> = [
572
573
  { name: "workos", command: options.packageManager, args: ["run", "forge", "--", "add", "auth", "workos", "--json"] },
574
+ { name: "generate-workos", command: options.packageManager, args: ["run", "forge", "--", "generate"] },
573
575
  { name: "authmd", command: options.packageManager, args: ["run", "forge", "--", "authmd", "generate", "--json"] },
576
+ { name: "authmd-check", command: options.packageManager, args: ["run", "forge", "--", "authmd", "check", "--json"] },
577
+ { name: "workos-doctor", command: options.packageManager, args: ["run", "forge", "--", "workos", "doctor", "--json"] },
578
+ { name: "workos-seed", command: options.packageManager, args: ["run", "forge", "--", "workos", "seed", "--file", "workos-seed.yml", "--dry-run", "--json"] },
574
579
  { name: "check", command: options.packageManager, args: ["run", "forge", "--", "check", "--json"] },
575
580
  ];
576
581
  for (const step of steps) {
@@ -232,8 +232,8 @@ export function buildAddJson(
232
232
  "forge workos install --yes --json",
233
233
  "forge workos doctor --json",
234
234
  "forge workos doctor --yes --json",
235
+ "forge workos seed --file workos-seed.yml --dry-run --json",
235
236
  "forge workos seed --file workos-seed.yml --json",
236
- "forge workos seed --file workos-seed.yml --yes --json",
237
237
  "forge auth check --json",
238
238
  "forge auth prove --json",
239
239
  ]
@@ -20,6 +20,8 @@ import type { AuthSubcommand } from "./auth.ts";
20
20
  import type { BaselineSubcommand } from "./baseline.ts";
21
21
  import type { AuthMdSubcommand } from "./authmd.ts";
22
22
  import type { WorkOSSubcommand } from "./workos.ts";
23
+ import type { DeploySubcommand, DeployTarget } from "./deploy.ts";
24
+ import type { FieldTestSubcommand } from "./field-test.ts";
23
25
  import type { RlsSubcommand } from "./rls.ts";
24
26
  import type { SecuritySubcommand } from "./security.ts";
25
27
  import type { DepsSubcommand } from "./deps.ts";
@@ -119,7 +121,7 @@ export type ForgeCommand =
119
121
  json: boolean;
120
122
  workspaceRoot: string;
121
123
  }
122
- | { kind: "doctor"; target?: "project" | "windows" | "agent" | "delta" | "pglite"; agentTarget?: AgentAdapterTarget; json: boolean; workspaceRoot: string }
124
+ | { kind: "doctor"; target?: "project" | "windows" | "agent" | "delta" | "pglite" | "runtime"; agentTarget?: AgentAdapterTarget; json: boolean; workspaceRoot: string }
123
125
  | { kind: "setup"; target: "windows"; json: boolean; yes: boolean; workspaceRoot: string }
124
126
  | {
125
127
  kind: "security";
@@ -155,6 +157,34 @@ export type ForgeCommand =
155
157
  dryRun: boolean;
156
158
  workspaceRoot: string;
157
159
  }
160
+ | {
161
+ kind: "deploy";
162
+ subcommand: DeploySubcommand;
163
+ target: DeployTarget;
164
+ production: boolean;
165
+ url?: string;
166
+ json: boolean;
167
+ workspaceRoot: string;
168
+ }
169
+ | {
170
+ kind: "field-test";
171
+ subcommand: FieldTestSubcommand;
172
+ name?: string;
173
+ template: NewTemplateName;
174
+ templates?: NewTemplateName[];
175
+ packageManager: NewPackageManager;
176
+ packageManagers?: NewPackageManager[];
177
+ forgeSpec?: string;
178
+ auth?: "none" | "workos";
179
+ dryRun: boolean;
180
+ keep: boolean;
181
+ runtimeProbes: boolean;
182
+ authProbes: boolean;
183
+ timeoutMs: number;
184
+ writeReport?: string;
185
+ json: boolean;
186
+ workspaceRoot: string;
187
+ }
158
188
  | {
159
189
  kind: "rls";
160
190
  subcommand: RlsSubcommand;
@@ -432,6 +462,8 @@ export const TOP_LEVEL_COMMANDS = [
432
462
  "auth",
433
463
  "authmd",
434
464
  "workos",
465
+ "deploy",
466
+ "field-test",
435
467
  "rls",
436
468
  "deps",
437
469
  "release",
@@ -544,6 +576,8 @@ const AUTH_SUBCOMMANDS: AuthSubcommand[] = [
544
576
  const BASELINE_SUBCOMMANDS: BaselineSubcommand[] = ["create", "status"];
545
577
  const AUTHMD_SUBCOMMANDS: AuthMdSubcommand[] = ["generate", "check"];
546
578
  const WORKOS_SUBCOMMANDS: WorkOSSubcommand[] = ["install", "doctor", "seed"];
579
+ const DEPLOY_SUBCOMMANDS: DeploySubcommand[] = ["plan", "check", "render", "verify"];
580
+ const FIELD_TEST_SUBCOMMANDS: FieldTestSubcommand[] = ["create", "run", "report"];
547
581
  const SECURITY_SUBCOMMANDS: SecuritySubcommand[] = ["prove"];
548
582
  const RLS_SUBCOMMANDS: RlsSubcommand[] = ["generate", "check", "apply", "test", "mutate-test"];
549
583
  const DEPS_SUBCOMMANDS: DepsSubcommand[] = [
@@ -811,6 +845,33 @@ function parseNewPackageManager(value: string | undefined): NewPackageManager {
811
845
  : "bun";
812
846
  }
813
847
 
848
+ function parseCommaList(value: string | undefined): string[] {
849
+ return String(value ?? "")
850
+ .split(",")
851
+ .map((item) => item.trim())
852
+ .filter(Boolean);
853
+ }
854
+
855
+ function parseTemplateList(value: string | undefined, errors: string[], optionName: string): NewTemplateName[] | undefined {
856
+ if (!value) return undefined;
857
+ const values = parseCommaList(value);
858
+ const unsupported = values.filter((item) => !NEW_TEMPLATES.includes(item as NewTemplateName));
859
+ if (unsupported.length > 0) {
860
+ errors.push(`${optionName} contains unsupported template(s): ${unsupported.join(", ")}; supported: ${NEW_TEMPLATES.join(", ")}`);
861
+ }
862
+ return values.filter((item) => NEW_TEMPLATES.includes(item as NewTemplateName)) as NewTemplateName[];
863
+ }
864
+
865
+ function parsePackageManagerList(value: string | undefined, errors: string[], optionName: string): NewPackageManager[] | undefined {
866
+ if (!value) return undefined;
867
+ const values = parseCommaList(value);
868
+ const unsupported = values.filter((item) => !NEW_PACKAGE_MANAGERS.includes(item as NewPackageManager));
869
+ if (unsupported.length > 0) {
870
+ errors.push(`${optionName} contains unsupported package manager(s): ${unsupported.join(", ")}; supported: ${NEW_PACKAGE_MANAGERS.join(", ")}`);
871
+ }
872
+ return values.filter((item) => NEW_PACKAGE_MANAGERS.includes(item as NewPackageManager)) as NewPackageManager[];
873
+ }
874
+
814
875
  function parseDoObjective(rest: string[], argv: string[]): string {
815
876
  const [action, name, ...tail] = rest;
816
877
  if (action === "add-resource") {
@@ -1255,8 +1316,8 @@ export function parseCli(argv: string[]): ParsedCli {
1255
1316
  };
1256
1317
  }
1257
1318
  case "doctor":
1258
- if (rest[0] && rest[0] !== "windows" && rest[0] !== "agent" && rest[0] !== "delta" && rest[0] !== "pglite") {
1259
- errors.push("forge doctor supports subcommand: windows, agent, delta, or pglite");
1319
+ if (rest[0] && rest[0] !== "windows" && rest[0] !== "agent" && rest[0] !== "delta" && rest[0] !== "pglite" && rest[0] !== "runtime") {
1320
+ errors.push("forge doctor supports subcommand: windows, agent, delta, pglite, or runtime");
1260
1321
  return { command: null, workspaceRoot, errors };
1261
1322
  }
1262
1323
  return {
@@ -1270,6 +1331,8 @@ export function parseCli(argv: string[]): ParsedCli {
1270
1331
  ? "delta"
1271
1332
  : rest[0] === "pglite"
1272
1333
  ? "pglite"
1334
+ : rest[0] === "runtime"
1335
+ ? "runtime"
1273
1336
  : "project",
1274
1337
  agentTarget: rest[0] === "agent"
1275
1338
  ? (parseOptionValue(argv, "--target") as AgentAdapterTarget | undefined) ?? (rest[1] as AgentAdapterTarget | undefined) ?? "codex"
@@ -1330,7 +1393,7 @@ export function parseCli(argv: string[]): ParsedCli {
1330
1393
  subcommand,
1331
1394
  json: parseFlag(argv, "--json"),
1332
1395
  token: parseOptionValue(argv, "--token"),
1333
- prod: parseFlag(argv, "--prod"),
1396
+ prod: parseFlag(argv, "--prod") || parseFlag(argv, "--production"),
1334
1397
  scenario: parseOptionValue(argv, "--scenario"),
1335
1398
  workspaceRoot,
1336
1399
  },
@@ -1376,6 +1439,71 @@ export function parseCli(argv: string[]): ParsedCli {
1376
1439
  errors,
1377
1440
  };
1378
1441
  }
1442
+ case "deploy": {
1443
+ const subcommand = rest[0] as DeploySubcommand | undefined;
1444
+ if (!subcommand || !DEPLOY_SUBCOMMANDS.includes(subcommand)) {
1445
+ errors.push("forge deploy requires subcommand: plan, check, render, or verify");
1446
+ return { command: null, workspaceRoot, errors };
1447
+ }
1448
+ const targetRaw = parseOptionValue(argv, "--target") ?? (subcommand === "render" ? rest[1] : undefined) ?? "docker";
1449
+ if (targetRaw !== "docker" && targetRaw !== "forge-cloud") {
1450
+ errors.push("forge deploy --target must be docker or forge-cloud");
1451
+ }
1452
+ return {
1453
+ command: {
1454
+ kind: "deploy",
1455
+ subcommand,
1456
+ target: targetRaw as DeployTarget,
1457
+ production: parseFlag(argv, "--production") || parseFlag(argv, "--prod"),
1458
+ url: parseOptionValue(argv, "--url"),
1459
+ json: parseFlag(argv, "--json"),
1460
+ workspaceRoot,
1461
+ },
1462
+ workspaceRoot,
1463
+ errors,
1464
+ };
1465
+ }
1466
+ case "field-test": {
1467
+ const subcommand = rest[0] as FieldTestSubcommand | undefined;
1468
+ if (!subcommand || !FIELD_TEST_SUBCOMMANDS.includes(subcommand)) {
1469
+ errors.push("forge field-test requires subcommand: create, run, or report");
1470
+ return { command: null, workspaceRoot, errors };
1471
+ }
1472
+ const timeoutRaw = parseOptionValue(argv, "--timeout-ms");
1473
+ const timeoutMs = timeoutRaw ? Number(timeoutRaw) : 180_000;
1474
+ if (!Number.isFinite(timeoutMs) || timeoutMs < 1) {
1475
+ errors.push("--timeout-ms must be a positive integer");
1476
+ }
1477
+ const authRaw = parseOptionValue(argv, "--auth") ?? "none";
1478
+ if (authRaw !== "none" && authRaw !== "workos") {
1479
+ errors.push("forge field-test --auth must be none or workos");
1480
+ }
1481
+ const templates = parseTemplateList(parseOptionValue(argv, "--templates"), errors, "--templates");
1482
+ const packageManagers = parsePackageManagerList(parseOptionValue(argv, "--package-managers"), errors, "--package-managers");
1483
+ return {
1484
+ command: {
1485
+ kind: "field-test",
1486
+ subcommand,
1487
+ name: subcommand === "create" ? rest[1] : undefined,
1488
+ template: parseNewTemplate(parseOptionValue(argv, "--template") ?? "minimal-web"),
1489
+ templates,
1490
+ packageManager: parseNewPackageManager(parseOptionValue(argv, "--package-manager") ?? "npm"),
1491
+ packageManagers,
1492
+ forgeSpec: parseOptionValue(argv, "--forge-spec"),
1493
+ auth: authRaw as "none" | "workos",
1494
+ dryRun: parseFlag(argv, "--dry-run"),
1495
+ keep: parseFlag(argv, "--keep"),
1496
+ runtimeProbes: parseFlag(argv, "--runtime-probes"),
1497
+ authProbes: parseFlag(argv, "--auth-probes"),
1498
+ timeoutMs: Math.floor(timeoutMs),
1499
+ writeReport: parseOptionValue(argv, "--write-report") ?? parseOptionValue(argv, "--file"),
1500
+ json: parseFlag(argv, "--json"),
1501
+ workspaceRoot,
1502
+ },
1503
+ workspaceRoot,
1504
+ errors,
1505
+ };
1506
+ }
1379
1507
  case "rls": {
1380
1508
  const subcommand = rest[0] as RlsSubcommand | undefined;
1381
1509
  if (!subcommand || !RLS_SUBCOMMANDS.includes(subcommand)) {
@@ -2860,6 +2988,10 @@ export function hasUnknownOption(argv: string[]): string | null {
2860
2988
  "--no-generate",
2861
2989
  "--no-verify",
2862
2990
  "--keep-failed",
2991
+ "--keep",
2992
+ "--runtime-probes",
2993
+ "--auth-probes",
2994
+ "--write-report",
2863
2995
  "--tenant-scoped",
2864
2996
  "--field",
2865
2997
  "--fields",
@@ -2913,6 +3045,7 @@ export function hasUnknownOption(argv: string[]): string | null {
2913
3045
  "--timeout",
2914
3046
  "--name",
2915
3047
  "--auth-token",
3048
+ "--auth",
2916
3049
  "--update",
2917
3050
  "--allow-high-risk",
2918
3051
  "--to",
@@ -2986,7 +3119,9 @@ export function hasUnknownOption(argv: string[]): string | null {
2986
3119
  "--prompt",
2987
3120
  "--url",
2988
3121
  "--template",
3122
+ "--templates",
2989
3123
  "--package-manager",
3124
+ "--package-managers",
2990
3125
  "--forge-spec",
2991
3126
  "--local-forge",
2992
3127
  "--install",
@@ -3014,6 +3149,7 @@ export function hasUnknownOption(argv: string[]): string | null {
3014
3149
  "--allow-dev-auth",
3015
3150
  "--token",
3016
3151
  "--prod",
3152
+ "--production",
3017
3153
  "--scenario",
3018
3154
  "--reason",
3019
3155
  "--no-preserve-user-sections",
@@ -3091,6 +3227,7 @@ export function hasUnknownOption(argv: string[]): string | null {
3091
3227
  arg === "--name" ||
3092
3228
  arg === "--reason" ||
3093
3229
  arg === "--auth-token" ||
3230
+ arg === "--auth" ||
3094
3231
  arg === "--sandbox-backend" ||
3095
3232
  arg === "--port" ||
3096
3233
  arg === "--host" ||
@@ -3107,6 +3244,7 @@ export function hasUnknownOption(argv: string[]): string | null {
3107
3244
  arg === "--step" ||
3108
3245
  arg === "--sink" ||
3109
3246
  arg === "--file" ||
3247
+ arg === "--write-report" ||
3110
3248
  arg === "--telemetry" ||
3111
3249
  arg === "--user-id" ||
3112
3250
  arg === "--tenant" ||
@@ -1,5 +1,6 @@
1
- import { existsSync, readFileSync } from "node:fs";
2
- import { join } from "node:path";
1
+ import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { basename, join } from "node:path";
3
4
  import { spawnSync } from "node:child_process";
4
5
  import { GENERATED_DIR } from "../compiler/emitter/constants.ts";
5
6
  import { stripDeterministicHeader } from "../compiler/primitives/header.ts";
@@ -84,6 +85,14 @@ function readText(root: string, path: string): string {
84
85
  return stripDeterministicHeader(readFileSync(absolute, "utf8"));
85
86
  }
86
87
 
88
+ function readRawText(root: string, path: string): string {
89
+ const absolute = join(root, path);
90
+ if (!existsSync(absolute)) {
91
+ return "";
92
+ }
93
+ return readFileSync(absolute, "utf8");
94
+ }
95
+
87
96
  function includesAll(haystack: string, needles: string[]): boolean {
88
97
  return needles.every((needle) => haystack.includes(needle));
89
98
  }
@@ -96,7 +105,7 @@ function quotedValues(text: string): string[] {
96
105
  return [...text.matchAll(/["']([^"']+)["']/g)].map((match) => match[1]!).filter(Boolean);
97
106
  }
98
107
 
99
- interface WorkOSSeedSummary {
108
+ export interface WorkOSSeedSummary {
100
109
  exists: boolean;
101
110
  valid: boolean;
102
111
  path: string;
@@ -118,7 +127,7 @@ function parseName(line: string): string | null {
118
127
  return match?.[1]?.trim() ?? null;
119
128
  }
120
129
 
121
- function parseSeedFile(workspaceRoot: string, preferredPath = DEFAULT_SEED_FILE): WorkOSSeedSummary {
130
+ export function parseSeedFile(workspaceRoot: string, preferredPath = DEFAULT_SEED_FILE): WorkOSSeedSummary {
122
131
  const seedPath = exists(workspaceRoot, preferredPath)
123
132
  ? preferredPath
124
133
  : exists(workspaceRoot, GENERATED_SEED_FILE)
@@ -200,7 +209,7 @@ function parseSeedFile(workspaceRoot: string, preferredPath = DEFAULT_SEED_FILE)
200
209
  };
201
210
  }
202
211
 
203
- function collectPolicyPermissions(workspaceRoot: string): string[] {
212
+ export function collectPolicyPermissions(workspaceRoot: string): string[] {
204
213
  const registry = readJson(workspaceRoot, `${GENERATED_DIR}/policyRegistry.json`) as {
205
214
  policies?: Array<{ permissions?: string[] }>;
206
215
  } | null;
@@ -234,7 +243,7 @@ function singularResourceName(name: string): string {
234
243
  return name;
235
244
  }
236
245
 
237
- function collectExpectedResourceTypes(workspaceRoot: string): string[] {
246
+ export function collectExpectedResourceTypes(workspaceRoot: string): string[] {
238
247
  const dataGraph = readJson(workspaceRoot, `${GENERATED_DIR}/dataGraph.json`) as {
239
248
  tables?: Array<{ name?: string; fields?: Array<{ name?: string }> }>;
240
249
  } | null;
@@ -257,7 +266,7 @@ function collectExpectedResourceTypes(workspaceRoot: string): string[] {
257
266
  return uniqueSorted(resourceTypes);
258
267
  }
259
268
 
260
- function missingValues(expected: string[], actual: string[]): string[] {
269
+ export function missingValues(expected: string[], actual: string[]): string[] {
261
270
  const actualSet = new Set(actual);
262
271
  return expected.filter((value) => !actualSet.has(value));
263
272
  }
@@ -389,6 +398,54 @@ function collectWorkOSChecks(workspaceRoot: string): WorkOSCheck[] {
389
398
  ];
390
399
  }
391
400
 
401
+ function prepareSeedFileForWorkOSCli(
402
+ workspaceRoot: string,
403
+ file: string,
404
+ ): { file: string; sanitized: boolean; cleanup: () => void } {
405
+ const raw = readRawText(workspaceRoot, file);
406
+ const stripped = stripDeterministicHeader(raw);
407
+ if (!raw || raw === stripped) {
408
+ return { file, sanitized: false, cleanup: () => undefined };
409
+ }
410
+
411
+ const tempDir = mkdtempSync(join(tmpdir(), "forge-workos-seed-"));
412
+ const preparedFile = join(tempDir, basename(file));
413
+ writeFileSync(preparedFile, stripped, "utf8");
414
+ return {
415
+ file: preparedFile,
416
+ sanitized: true,
417
+ cleanup: () => rmSync(tempDir, { recursive: true, force: true }),
418
+ };
419
+ }
420
+
421
+ function isWorkOSSeedAlreadyApplied(stdout: string, stderr: string): boolean {
422
+ const output = `${stdout}\n${stderr}`;
423
+ return /(?:permission|role|resource type|organization|slug|domain)[^\n]*(?:already in use|already exists|exists already)|already in use/i
424
+ .test(output);
425
+ }
426
+
427
+ function seedData(input: {
428
+ seed: WorkOSSeedSummary;
429
+ activePermissions: string[];
430
+ expectedResourceTypes: string[];
431
+ unusedSeedPermissions: string[];
432
+ dryRun?: boolean;
433
+ seedFileSanitized?: boolean;
434
+ seedAlreadyApplied?: boolean;
435
+ nextCommand?: string;
436
+ }): Record<string, unknown> {
437
+ return {
438
+ seed: input.seed,
439
+ activePermissions: input.activePermissions,
440
+ expectedResourceTypes: input.expectedResourceTypes,
441
+ unusedSeedPermissions: input.unusedSeedPermissions,
442
+ dryRun: input.dryRun ?? false,
443
+ seedFileSanitized: input.seedFileSanitized ?? false,
444
+ seedAlreadyApplied: input.seedAlreadyApplied ?? false,
445
+ ...(input.nextCommand ? { nextCommand: input.nextCommand } : {}),
446
+ };
447
+ }
448
+
392
449
  export function runWorkOSDoctorCommand(options: WorkOSCommandOptions): WorkOSCommandResult {
393
450
  const checks = collectWorkOSChecks(options.workspaceRoot);
394
451
  const ok = checks.every((check) => check.ok);
@@ -505,33 +562,62 @@ export function runWorkOSSeedCommand(options: WorkOSCommandOptions): WorkOSComma
505
562
  checks,
506
563
  command,
507
564
  applied: false,
508
- data: { seed, activePermissions, expectedResourceTypes, unusedSeedPermissions },
565
+ data: seedData({ seed, activePermissions, expectedResourceTypes, unusedSeedPermissions }),
509
566
  exitCode: 1,
510
567
  };
511
568
  }
512
- if (!options.yes || options.dryRun) {
569
+ if (options.dryRun) {
513
570
  return {
514
571
  ok: true,
515
572
  kind: "workos-seed",
516
573
  checks,
517
574
  command,
518
575
  applied: false,
519
- data: { seed, activePermissions, expectedResourceTypes, unusedSeedPermissions },
576
+ data: seedData({
577
+ seed,
578
+ activePermissions,
579
+ expectedResourceTypes,
580
+ unusedSeedPermissions,
581
+ dryRun: true,
582
+ nextCommand: `forge workos seed --file ${file} --json`,
583
+ }),
520
584
  exitCode: 0,
521
585
  };
522
586
  }
523
- const child = runExternalCommand(command, options);
524
- return {
525
- ok: child.status === 0,
526
- kind: "workos-seed",
527
- checks,
528
- command,
529
- applied: child.status === 0,
530
- data: { seed, activePermissions, expectedResourceTypes, unusedSeedPermissions },
531
- stdout: child.stdout,
532
- stderr: child.stderr,
533
- exitCode: child.status === 0 ? 0 : 1,
534
- };
587
+ const preparedSeed = prepareSeedFileForWorkOSCli(options.workspaceRoot, seed.path);
588
+ const delegatedCommand = [
589
+ "npx",
590
+ "--yes",
591
+ "workos@latest",
592
+ "seed",
593
+ "--file",
594
+ preparedSeed.file,
595
+ ];
596
+ try {
597
+ const child = runExternalCommand(delegatedCommand, options);
598
+ const seedAlreadyApplied =
599
+ child.status !== 0 && isWorkOSSeedAlreadyApplied(child.stdout, child.stderr);
600
+ return {
601
+ ok: child.status === 0 || seedAlreadyApplied,
602
+ kind: "workos-seed",
603
+ checks,
604
+ command: delegatedCommand,
605
+ applied: child.status === 0,
606
+ data: seedData({
607
+ seed,
608
+ activePermissions,
609
+ expectedResourceTypes,
610
+ unusedSeedPermissions,
611
+ seedFileSanitized: preparedSeed.sanitized,
612
+ seedAlreadyApplied,
613
+ }),
614
+ stdout: child.stdout,
615
+ stderr: child.stderr,
616
+ exitCode: child.status === 0 || seedAlreadyApplied ? 0 : 1,
617
+ };
618
+ } finally {
619
+ preparedSeed.cleanup();
620
+ }
535
621
  }
536
622
 
537
623
  export function runWorkOSCommand(options: WorkOSCommandOptions): WorkOSCommandResult {
@@ -562,7 +648,16 @@ export function formatWorkOSHuman(result: WorkOSCommandResult): string {
562
648
  lines.push("external WorkOS doctor not run; pass --yes to execute the WorkOS CLI command");
563
649
  }
564
650
  if (result.kind === "workos-seed" && !result.applied) {
565
- lines.push("seed not applied; pass --yes to execute the WorkOS CLI command");
651
+ const data = result.data && typeof result.data === "object"
652
+ ? result.data as { dryRun?: boolean; seedAlreadyApplied?: boolean; nextCommand?: string }
653
+ : {};
654
+ if (data.seedAlreadyApplied) {
655
+ lines.push("seed already appears applied; WorkOS reported existing resources");
656
+ } else if (data.dryRun) {
657
+ lines.push(`seed dry-run only; run ${data.nextCommand ?? "forge workos seed --file workos-seed.yml --json"} to execute the WorkOS CLI command`);
658
+ } else {
659
+ lines.push("seed not applied; inspect stdout/stderr from the WorkOS CLI command");
660
+ }
566
661
  }
567
662
  return `${lines.join("\n")}\n`;
568
663
  }
@@ -41,11 +41,23 @@ export interface IntegrationPlanInput {
41
41
  existingLock: ForgeLock | null;
42
42
  }
43
43
 
44
+ function headerForGeneratedPath(path: string): EmitFile["header"] {
45
+ return path.endsWith(".yml") || path.endsWith(".yaml") ? "none" : undefined;
46
+ }
47
+
48
+ function headerForRootFile(path: string): EmitFile["header"] {
49
+ if (path.endsWith(".env.example") || path.endsWith(".yml") || path.endsWith(".yaml")) {
50
+ return "none";
51
+ }
52
+ return "deterministic";
53
+ }
54
+
44
55
  function makeEmitFile(path: string, content: string): EmitFile {
45
56
  return {
46
57
  path,
47
58
  content,
48
59
  contentHash: hashStable(content),
60
+ header: headerForGeneratedPath(path),
49
61
  };
50
62
  }
51
63
 
@@ -236,7 +248,7 @@ export function buildIntegrationEmitPlan(input: IntegrationPlanInput): EmitPlan
236
248
  path: rootFile,
237
249
  content,
238
250
  contentHash: hashStable(content),
239
- header: rootFile.endsWith(".env.example") ? "none" : "deterministic",
251
+ header: headerForRootFile(rootFile),
240
252
  });
241
253
  }
242
254
 
@@ -184,8 +184,8 @@ export function renderWorkosAuthkit(_input: IntegrationTemplateInput): string {
184
184
  ' "forge workos install --yes --json",',
185
185
  ' "forge workos doctor --json",',
186
186
  ' "forge workos doctor --yes --json",',
187
+ ' "forge workos seed --file workos-seed.yml --dry-run --json",',
187
188
  ' "forge workos seed --file workos-seed.yml --json",',
188
- ' "forge workos seed --file workos-seed.yml --yes --json",',
189
189
  ' "forge auth check --json",',
190
190
  ' "forge auth prove --json",',
191
191
  "] as const;",
@@ -1077,8 +1077,8 @@ export function renderWorkosDoc(input: IntegrationTemplateInput): string {
1077
1077
  "forge workos install --yes --json",
1078
1078
  "forge workos doctor --json",
1079
1079
  "forge workos doctor --yes --json",
1080
+ "forge workos seed --file workos-seed.yml --dry-run --json",
1080
1081
  "forge workos seed --file workos-seed.yml --json",
1081
- "forge workos seed --file workos-seed.yml --yes --json",
1082
1082
  "forge auth check --json",
1083
1083
  "forge auth prove --json",
1084
1084
  "forge check --json",
@@ -1,3 +1,3 @@
1
- export const FORGEOS_VERSION = "0.1.0-alpha.37";
1
+ export const FORGEOS_VERSION = "0.1.0-alpha.39";
2
2
  export const GENERATOR_VERSION = FORGEOS_VERSION;
3
3
  export const CLI_VERSION = FORGEOS_VERSION;