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.
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +15 -0
- package/docs/changelog.md +37 -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/auth.ts +171 -13
- package/src/forge/cli/commands.ts +73 -5
- package/src/forge/cli/deploy.ts +887 -0
- package/src/forge/cli/docs.ts +1 -1
- package/src/forge/cli/doctor.ts +216 -0
- package/src/forge/cli/field-test.ts +328 -0
- package/src/forge/cli/handoff.ts +13 -5
- package/src/forge/cli/main.ts +11 -2
- package/src/forge/cli/new.ts +5 -0
- package/src/forge/cli/output.ts +1 -1
- package/src/forge/cli/parse.ts +142 -4
- package/src/forge/cli/workos.ts +118 -23
- package/src/forge/compiler/integration/plan.ts +13 -1
- package/src/forge/compiler/integration/templates/workos.ts +2 -2
- package/src/forge/version.ts +1 -1
package/src/forge/cli/main.ts
CHANGED
|
@@ -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
|
|
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
|
|
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",
|
package/src/forge/cli/new.ts
CHANGED
|
@@ -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) {
|
package/src/forge/cli/output.ts
CHANGED
|
@@ -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
|
]
|
package/src/forge/cli/parse.ts
CHANGED
|
@@ -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
|
|
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" ||
|
package/src/forge/cli/workos.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
-
import {
|
|
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 (
|
|
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: {
|
|
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
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
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
|
-
|
|
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
|
|
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",
|
package/src/forge/version.ts
CHANGED