@fjall/util 20.0.0 → 21.1.0
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/dist/.build-source-hash +25 -14
- package/dist/.minified +1 -1
- package/dist/atomicTempNaming.d.ts +26 -0
- package/dist/atomicTempNaming.js +1 -0
- package/dist/aws/costAllocationTags.d.ts +18 -0
- package/dist/aws/costAllocationTags.js +1 -1
- package/dist/aws/index.d.ts +1 -1
- package/dist/aws/index.js +1 -1
- package/dist/config.d.ts +28 -14
- package/dist/config.js +2 -2
- package/dist/configPaths.d.ts +22 -0
- package/dist/configPaths.js +1 -0
- package/dist/fjallHome.d.ts +78 -0
- package/dist/fjallHome.js +1 -0
- package/dist/fsHelpers.d.ts +9 -0
- package/dist/fsHelpers.js +1 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.js +1 -1
- package/dist/logRotation.d.ts +42 -0
- package/dist/logRotation.js +1 -0
- package/dist/migration/clickhouseFrameworkUsers.d.ts +80 -0
- package/dist/migration/clickhouseFrameworkUsers.js +1 -0
- package/dist/migration/clickhouseFrameworkUsers.test.d.ts +1 -0
- package/dist/migration/clickhouseFrameworkUsers.test.js +1 -0
- package/dist/migration/constants.d.ts +29 -7
- package/dist/migration/constants.js +1 -1
- package/dist/migration/controlPlaneActivity.d.ts +58 -0
- package/dist/migration/controlPlaneActivity.js +2 -0
- package/dist/migration/index.d.ts +5 -1
- package/dist/migration/index.js +1 -1
- package/dist/migration/schemaGateExit.d.ts +63 -0
- package/dist/migration/schemaGateExit.js +1 -0
- package/dist/migration/sleepAbortable.d.ts +10 -0
- package/dist/migration/sleepAbortable.js +1 -0
- package/dist/patterns/frameworkPatterns.d.ts +151 -0
- package/dist/patterns/frameworkPatterns.js +1 -0
- package/dist/patterns/index.d.ts +2 -1
- package/dist/patterns/index.js +1 -1
- package/dist/patterns/patternTypes.d.ts +29 -0
- package/dist/patterns/patternTypes.js +1 -1
- package/dist/scaffold/ensureGitignore.d.ts +18 -0
- package/dist/scaffold/ensureGitignore.js +9 -0
- package/dist/scaffold/gitignore.d.ts +47 -0
- package/dist/scaffold/gitignore.js +35 -0
- package/dist/securityHelpers.d.ts +1 -1
- package/dist/securityHelpers.js +1 -1
- package/package.json +9 -1
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-package contract for the framework-owned ClickHouse identities —
|
|
3
|
+
* one SQL user per control-plane workload class, so no two classes ever
|
|
4
|
+
* contend for the same per-user concurrency cap.
|
|
5
|
+
*
|
|
6
|
+
* | identity | holder(s) | rights |
|
|
7
|
+
* | -------------------- | -------------------------------------- | ---------------------------------------- |
|
|
8
|
+
* | `schemaAdmin.name` | migration task (XML, `ddl_admin`) | full DDL; cap 1 = DDL single-flight |
|
|
9
|
+
* | `fjall_schema_gate` | materialised gate container | `readonly=2`, SELECT on `_schema_migrations` only; NO per-user cap |
|
|
10
|
+
* | `fjall_maintenance` | backup + optimise sidecars | today's `ddl_admin` numbers (cap 1); OPTIMIZE / BACKUP / RESTORE / scratch DROP |
|
|
11
|
+
*
|
|
12
|
+
* Producer: `ClickHouseDatabase` mints a Secrets-Manager password per
|
|
13
|
+
* framework identity (OUTSIDE the customer `managedPasswords` set — these
|
|
14
|
+
* secrets are never injected into the ClickHouse service container, so
|
|
15
|
+
* adopting them changes no ClickHouse task definition) and
|
|
16
|
+
* `getMigrationContributions()` emits `CLICKHOUSE_FRAMEWORK_USERS` (a
|
|
17
|
+
* JSON-stringified array of the names below) plus a `USER_<NAME>_PASSWORD`
|
|
18
|
+
* import per identity on the migration task.
|
|
19
|
+
*
|
|
20
|
+
* Consumer: `@fjall/clickhouse § provisionFrameworkUsersFromEnv` runs as
|
|
21
|
+
* `schemaAdmin` from the pre-scale-up lifecycle hook — ordered ahead of every
|
|
22
|
+
* gate and sidecar — and issues the idempotent CREATE / ALTER / GRANT set for
|
|
23
|
+
* each identity, so the identities exist before anything authenticates as
|
|
24
|
+
* them. SQL-provisioned (not `users.xml`) deliberately: no ClickHouse
|
|
25
|
+
* restart, no `SYSTEM RELOAD USERS` race, zero outage on adoption.
|
|
26
|
+
*
|
|
27
|
+
* Coupled values — must move together per
|
|
28
|
+
* `.claude/rules/code-quality.md § "Coupled values: shared source at 2
|
|
29
|
+
* occurrences"`.
|
|
30
|
+
*/
|
|
31
|
+
import { z } from "zod";
|
|
32
|
+
/** Read-only identity the materialised schema gate authenticates as. */
|
|
33
|
+
export declare const FJALL_SCHEMA_GATE_USER: "fjall_schema_gate";
|
|
34
|
+
/** Identity the backup + optimise maintenance sidecars authenticate as. */
|
|
35
|
+
export declare const FJALL_MAINTENANCE_USER: "fjall_maintenance";
|
|
36
|
+
/**
|
|
37
|
+
* Every framework-owned identity, in provisioning order. Derive from this —
|
|
38
|
+
* never re-list the names — so a new workload class lands in the manifest,
|
|
39
|
+
* the reserved-name validator, and the provisioning loop at once.
|
|
40
|
+
*/
|
|
41
|
+
export declare const FJALL_CLICKHOUSE_USERS: readonly ["fjall_schema_gate", "fjall_maintenance"];
|
|
42
|
+
export type FjallClickHouseUser = (typeof FJALL_CLICKHOUSE_USERS)[number];
|
|
43
|
+
/**
|
|
44
|
+
* Prefix reserved for framework identities. `ClickHouseDatabase` throws at
|
|
45
|
+
* synth when a customer `schemaAdmin.name` or `managedPasswords` entry
|
|
46
|
+
* starts with it, so a customer user can never collide with (or be silently
|
|
47
|
+
* re-provisioned as) a framework identity.
|
|
48
|
+
*/
|
|
49
|
+
export declare const FJALL_CLICKHOUSE_USER_PREFIX: "fjall_";
|
|
50
|
+
export declare function isReservedClickHouseUserName(name: string): boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Container env var name carrying the JSON-stringified manifest of framework
|
|
53
|
+
* identities to provision. Sibling to `CLICKHOUSE_MANAGED_USERS` (customer
|
|
54
|
+
* users); kept separate because the two sets differ in ownership — the
|
|
55
|
+
* framework decides its identities' rights, customer SQL decides the managed
|
|
56
|
+
* users' profiles.
|
|
57
|
+
*/
|
|
58
|
+
export declare const CLICKHOUSE_FRAMEWORK_USERS_ENV: "CLICKHOUSE_FRAMEWORK_USERS";
|
|
59
|
+
/**
|
|
60
|
+
* Container env var name carrying the settings-profile name the maintenance
|
|
61
|
+
* identity binds to — the construct contributes `schemaAdmin.profile` (the
|
|
62
|
+
* `ddl_admin` profile at the resolved instance's scale), so
|
|
63
|
+
* `fjall_maintenance` inherits today's memory / execution caps and tracks
|
|
64
|
+
* instance resizes without the runner knowing any numbers. The runner pins
|
|
65
|
+
* `max_concurrent_queries_for_user = 1 CONST` on top: a bare setting is a
|
|
66
|
+
* default the session can override, only `CONST` makes the cap binding.
|
|
67
|
+
*/
|
|
68
|
+
export declare const CLICKHOUSE_MAINTENANCE_PROFILE_ENV: "CLICKHOUSE_MAINTENANCE_PROFILE";
|
|
69
|
+
export declare const FrameworkUserNameSchema: z.ZodString & z.ZodType<"fjall_schema_gate" | "fjall_maintenance", string, z.core.$ZodTypeInternals<"fjall_schema_gate" | "fjall_maintenance", string>>;
|
|
70
|
+
export type FrameworkUserName = z.infer<typeof FrameworkUserNameSchema>;
|
|
71
|
+
export declare const FrameworkUserNamesSchema: z.ZodArray<z.ZodString & z.ZodType<"fjall_schema_gate" | "fjall_maintenance", string, z.core.$ZodTypeInternals<"fjall_schema_gate" | "fjall_maintenance", string>>>;
|
|
72
|
+
export type FrameworkUserNames = z.infer<typeof FrameworkUserNamesSchema>;
|
|
73
|
+
/**
|
|
74
|
+
* Scratch database the backup sidecar restores into to verify a backup, and
|
|
75
|
+
* the only database `fjall_maintenance` may DROP. Coupled across the
|
|
76
|
+
* construct (backup script, IAM) and `@fjall/clickhouse` (the GRANT set) —
|
|
77
|
+
* a drift would grant DROP on a database the script never touches while the
|
|
78
|
+
* one it does touch fails with `ACCESS_DENIED`.
|
|
79
|
+
*/
|
|
80
|
+
export declare const CLICKHOUSE_BACKUP_SCRATCH_DATABASE: "fjall_backup_verify";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var r=Object.defineProperty;var E=(e,o)=>r(e,"name",{value:o,configurable:!0});import{z as _}from"zod";import{MANAGED_USER_NAME_PATTERN as s}from"./clickhouseSqlUsers.js";const S="fjall_schema_gate",a="fjall_maintenance",t=[S,a],A="fjall_";function L(e){return e.startsWith(A)}E(L,"isReservedClickHouseUserName");const N="CLICKHOUSE_FRAMEWORK_USERS",m="CLICKHOUSE_MAINTENANCE_PROFILE",n=new Set(t),C=_.string().regex(s,"Must be lowercase snake_case").refine(e=>n.has(e),{message:`Must be one of ${t.join(", ")}`}),p=_.array(C),I="fjall_backup_verify";export{I as CLICKHOUSE_BACKUP_SCRATCH_DATABASE,N as CLICKHOUSE_FRAMEWORK_USERS_ENV,m as CLICKHOUSE_MAINTENANCE_PROFILE_ENV,t as FJALL_CLICKHOUSE_USERS,A as FJALL_CLICKHOUSE_USER_PREFIX,a as FJALL_MAINTENANCE_USER,S as FJALL_SCHEMA_GATE_USER,C as FrameworkUserNameSchema,p as FrameworkUserNamesSchema,L as isReservedClickHouseUserName};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{describe as a,expect as e,it as s}from"vitest";import{CLICKHOUSE_FRAMEWORK_USERS_ENV as f,FJALL_CLICKHOUSE_USER_PREFIX as C,FJALL_CLICKHOUSE_USERS as c,FJALL_MAINTENANCE_USER as l,FJALL_SCHEMA_GATE_USER as h,FrameworkUserNamesSchema as d,isReservedClickHouseUserName as n}from"./clickhouseFrameworkUsers.js";import{MANAGED_USER_NAME_PATTERN as A}from"./clickhouseSqlUsers.js";import{CONTROL_PLANE_ACTIVITY_RECENCY_SECONDS as S,buildControlPlaneActivityQuery as r}from"./controlPlaneActivity.js";import{SCHEMA_GATE_EXIT as i,SCHEMA_GATE_EXIT_HINTS as y,schemaGateExitKind as m}from"./schemaGateExit.js";import{sleepAbortable as E}from"./sleepAbortable.js";a("framework ClickHouse identities",()=>{s("every framework identity carries the reserved prefix and is a valid SQL name",()=>{for(const t of c)e(n(t)).toBe(!0),e(t).toMatch(A);e(c).toEqual([h,l])}),s("customer names outside the prefix are not reserved",()=>{e(n("schema_admin")).toBe(!1),e(n("backup_reader")).toBe(!1),e(n(`${C}x`)).toBe(!0)}),s("the manifest schema accepts only framework identities",()=>{e(d.safeParse([...c]).success).toBe(!0),e(d.safeParse(["fjall_other"]).success).toBe(!1),e(d.safeParse(["schema_admin"]).success).toBe(!1),e(f).toBe("CLICKHOUSE_FRAMEWORK_USERS")})}),a("schema gate exit taxonomy",()=>{s("codes are distinct and every kind has a hint",()=>{const t=Object.values(i);e(new Set(t).size).toBe(t.length);for(const o of Object.keys(i))e(y[o].length).toBeGreaterThan(0),e(m(i[o])).toBe(o)}),s("pins the wire values consumers already depend on",()=>{e(i).toEqual({pass:0,refused:1,configError:2,connectionError:3,busy:4,denied:5})}),s("returns undefined for codes the gate never emits",()=>{e(m(143)).toBeUndefined(),e(m(137)).toBeUndefined()})}),a("buildControlPlaneActivityQuery",()=>{s("probes both live processes and recent query_log for the named users",()=>{const t=r({users:["schema_admin",l]});e(t).toContain("FROM system.processes WHERE user IN ('schema_admin', 'fjall_maintenance')"),e(t).toContain("FROM system.query_log WHERE user IN ('schema_admin', 'fjall_maintenance')"),e(t).toContain(`INTERVAL ${S} SECOND`),e(t).toContain("type != 'QueryStart'"),e(t).toContain("event_date >= yesterday()"),e(t.trim().endsWith("AS active")).toBe(!0)}),s("honours a recency override",()=>{e(r({users:["schema_admin"],recencySeconds:5})).toContain("INTERVAL 5 SECOND")}),s("rejects names that could smuggle quoting, empty user lists, and bad recency",()=>{e(()=>r({users:["x' OR 1=1 --"]})).toThrow(/Invalid ClickHouse user name/),e(()=>r({users:[]})).toThrow(/at least one user/),e(()=>r({users:["a"],recencySeconds:0})).toThrow(/positive integer/)})}),a("sleepAbortable",()=>{s("resolves immediately on an already-aborted signal and on abort mid-sleep",async()=>{const t=new AbortController;t.abort(),await e(E(6e4,t.signal)).resolves.toBeUndefined();const o=new AbortController,u=Date.now(),_=E(6e4,o.signal);o.abort(),await _,e(Date.now()-u).toBeLessThan(1e3)})});
|
|
@@ -36,13 +36,13 @@ export declare const EXPECTED_SCHEMA_VERSION_TOOL_ENV: "EXPECTED_SCHEMA_VERSION_
|
|
|
36
36
|
export declare const EXPECTED_CH_SCHEMA_VERSION_ENV: "EXPECTED_CH_SCHEMA_VERSION";
|
|
37
37
|
/**
|
|
38
38
|
* Container env / secret names for the ClickHouse schema-admin user contributed
|
|
39
|
-
* by `ClickHouseDatabase`.
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
39
|
+
* by `ClickHouseDatabase`. Injected ONLY into the migration task (the sole
|
|
40
|
+
* DDL principal) — never into app containers, the gate container, or the
|
|
41
|
+
* maintenance sidecars, each of which authenticates as its own workload-class
|
|
42
|
+
* identity (see `clickhouseFrameworkUsers.ts`). One XML principal shared by
|
|
43
|
+
* six holders under a `max_concurrent_queries_for_user = 1` cap was the
|
|
44
|
+
* 2026-08-25 deploy-rollback incident: a gate probe lost the slot to the
|
|
45
|
+
* scheduled OPTIMIZE and reported the server "unreachable".
|
|
46
46
|
*/
|
|
47
47
|
export declare const SCHEMA_ADMIN_USER_ENV: "SCHEMA_ADMIN_USER";
|
|
48
48
|
export declare const SCHEMA_ADMIN_PASSWORD_ENV: "SCHEMA_ADMIN_PASSWORD";
|
|
@@ -76,6 +76,28 @@ export declare const SCHEMA_GATE_DB_PASSWORD_ENV: "FJALL_SCHEMA_GATE_DB_PASSWORD
|
|
|
76
76
|
export declare const SCHEMA_GATE_CH_URL_ENV: "CLICKHOUSE_URL";
|
|
77
77
|
export declare const SCHEMA_GATE_CH_DATABASE_ENV: "CLICKHOUSE_DATABASE";
|
|
78
78
|
export declare const SCHEMA_GATE_CH_CA_CERT_ENV: "CLICKHOUSE_CA_CERT";
|
|
79
|
+
/**
|
|
80
|
+
* ClickHouse credential env names for the gate container's CH half. The
|
|
81
|
+
* value of `SCHEMA_GATE_CH_USER_ENV` is always `FJALL_SCHEMA_GATE_USER` (the
|
|
82
|
+
* read-only gate identity), and `SCHEMA_GATE_CH_PASSWORD_ENV` is a
|
|
83
|
+
* Secrets-Manager import of that identity's password. Named in the
|
|
84
|
+
* `FJALL_SCHEMA_GATE_*` family alongside the Postgres trio so a reader of the
|
|
85
|
+
* task definition sees one gate contract, not a gate half and a borrowed
|
|
86
|
+
* admin half.
|
|
87
|
+
*/
|
|
88
|
+
export declare const SCHEMA_GATE_CH_USER_ENV: "FJALL_SCHEMA_GATE_CH_USER";
|
|
89
|
+
export declare const SCHEMA_GATE_CH_PASSWORD_ENV: "FJALL_SCHEMA_GATE_CH_PASSWORD";
|
|
90
|
+
/**
|
|
91
|
+
* ECS `startTimeout` for the synthetic gate container — the ceiling ECS
|
|
92
|
+
* allows the gate to reach its exit before the task is stopped as a failed
|
|
93
|
+
* start. The gate runner derives its own retry budget FROM this value (it
|
|
94
|
+
* must finish, with a verdict, comfortably inside it), so the two are coupled
|
|
95
|
+
* across the construct↔runner boundary: a construct that raised the timeout
|
|
96
|
+
* without the runner widening its budget would leave headroom unused; a
|
|
97
|
+
* runner budget exceeding the timeout would be cut off mid-retry with no
|
|
98
|
+
* verdict at all. Hoisted so both read one number.
|
|
99
|
+
*/
|
|
100
|
+
export declare const SCHEMA_GATE_START_TIMEOUT_SECONDS: 120;
|
|
79
101
|
/**
|
|
80
102
|
* The synthetic gate container's reserved name. Shared between the construct
|
|
81
103
|
* (container synthesis + the name-collision validator in `validateEcsProps`)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
const
|
|
1
|
+
const _="fjall-premigrate",E="EXPECTED_SCHEMA_VERSION",A="EXPECTED_SCHEMA_VERSION_TOOL",S="EXPECTED_CH_SCHEMA_VERSION",C="SCHEMA_ADMIN_USER",t="SCHEMA_ADMIN_PASSWORD",o="FJALL_SCHEMA_GATE_DB_URL_BASE",H="FJALL_SCHEMA_GATE_DB_USER",M="FJALL_SCHEMA_GATE_DB_PASSWORD",R="CLICKHOUSE_URL",T="CLICKHOUSE_DATABASE",N="CLICKHOUSE_CA_CERT",I="FJALL_SCHEMA_GATE_CH_USER",e="FJALL_SCHEMA_GATE_CH_PASSWORD",O=120,s="fjall-schema-gate",c="fjall/schema-gate",D="public.ecr.aws/fjall/schema-gate",r=/^\d{14}_/,p=/\.dev\.sql$/,L=/\.sql$/;export{L as CLICKHOUSE_MIGRATION_FILE_RE,p as CLICKHOUSE_MIGRATION_SKIP_RE,S as EXPECTED_CH_SCHEMA_VERSION_ENV,E as EXPECTED_SCHEMA_VERSION_ENV,A as EXPECTED_SCHEMA_VERSION_TOOL_ENV,_ as MIGRATION_SNAPSHOT_NAME_PREFIX,r as PRISMA_MIGRATION_DIR_RE,t as SCHEMA_ADMIN_PASSWORD_ENV,C as SCHEMA_ADMIN_USER_ENV,N as SCHEMA_GATE_CH_CA_CERT_ENV,T as SCHEMA_GATE_CH_DATABASE_ENV,e as SCHEMA_GATE_CH_PASSWORD_ENV,R as SCHEMA_GATE_CH_URL_ENV,I as SCHEMA_GATE_CH_USER_ENV,s as SCHEMA_GATE_CONTAINER_NAME,M as SCHEMA_GATE_DB_PASSWORD_ENV,o as SCHEMA_GATE_DB_URL_BASE_ENV,H as SCHEMA_GATE_DB_USER_ENV,c as SCHEMA_GATE_ECR_REPO_NAME,D as SCHEMA_GATE_PUBLIC_IMAGE_REPO,O as SCHEMA_GATE_START_TIMEOUT_SECONDS};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ClickHouse-native activity probe for the migrate ↔ maintenance mutex.
|
|
3
|
+
*
|
|
4
|
+
* The migration task (Compute stack) and the maintenance sidecars (Database
|
|
5
|
+
* stack) cannot see each other through ECS — the Database stack is deployed
|
|
6
|
+
* first and cannot reference the Compute stack — so the mutual exclusion is
|
|
7
|
+
* observed through the server both already talk to: `system.processes`
|
|
8
|
+
* (queries running right now) plus `system.query_log` recency (a query that
|
|
9
|
+
* finished within the last `recencySeconds`). Each side asks about the OTHER
|
|
10
|
+
* control-plane identity and defers while it is active:
|
|
11
|
+
*
|
|
12
|
+
* - the maintenance wrapper asks about `schemaAdmin.name` before OPTIMIZE /
|
|
13
|
+
* BACKUP and exits `deferred` (exit 0, metric) when a migration is under way;
|
|
14
|
+
* - the migration runner asks about `fjall_maintenance` before its ClickHouse
|
|
15
|
+
* phase and waits (bounded, abort-aware) for the sidecar to go idle.
|
|
16
|
+
*
|
|
17
|
+
* The recency window closes the gap between one statement of a multi-
|
|
18
|
+
* statement job finishing and the next one starting (there is no process
|
|
19
|
+
* row in between). Both sides read the same builder so the two halves of
|
|
20
|
+
* the mutex cannot drift in what "active" means.
|
|
21
|
+
*
|
|
22
|
+
* `system.query_log` is flushed on an interval (7500 ms in the constructs'
|
|
23
|
+
* server config), so the recency signal lands up to one flush late; the
|
|
24
|
+
* residual window is the sub-second gap between a process row vanishing and
|
|
25
|
+
* its finish row landing, and the two identities carry separate concurrency
|
|
26
|
+
* caps, so a probe that slips through it costs contention, never a 202. Two
|
|
27
|
+
* server-side preconditions keep the recency half honest: the table only
|
|
28
|
+
* exists after the first flush (a brand-new server answers `Code: 60` until
|
|
29
|
+
* then — the probe's own queries create it inside one interval, which is why
|
|
30
|
+
* `awaitControlPlaneIdle` keeps polling through non-denied errors), and
|
|
31
|
+
* `log_queries_min_query_duration_ms` must stay 0 (see `clickhouseTuning`),
|
|
32
|
+
* else short control-plane statements never reach the log at all.
|
|
33
|
+
*
|
|
34
|
+
* The caller's own identity is never in `users`, so no self-exclusion is
|
|
35
|
+
* needed; the probe is a plain SELECT the gate/maintenance/admin identities
|
|
36
|
+
* are all granted on `system.processes` and `system.query_log`.
|
|
37
|
+
*/
|
|
38
|
+
/**
|
|
39
|
+
* How long after a control-plane query completes the identity is still
|
|
40
|
+
* considered active. Long enough to bridge a sidecar's per-statement gaps
|
|
41
|
+
* (the backup script runs DROP → BACKUP → RESTORE back-to-back), short
|
|
42
|
+
* enough that a finished job releases the mutex well inside one deploy.
|
|
43
|
+
*/
|
|
44
|
+
export declare const CONTROL_PLANE_ACTIVITY_RECENCY_SECONDS: 120;
|
|
45
|
+
export interface ControlPlaneActivityQueryOpts {
|
|
46
|
+
/** Identities to test for activity — the OTHER side of the mutex. */
|
|
47
|
+
users: readonly string[];
|
|
48
|
+
/** Overrides `CONTROL_PLANE_ACTIVITY_RECENCY_SECONDS`. */
|
|
49
|
+
recencySeconds?: number;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Builds a single-row `SELECT active` returning `1` when any of `users` has
|
|
53
|
+
* a running query or finished one within the recency window, else `0`.
|
|
54
|
+
* Names are validated against the managed-user pattern before interpolation
|
|
55
|
+
* — the only quoting the SQL needs — so a name that could carry a quote
|
|
56
|
+
* never reaches the string.
|
|
57
|
+
*/
|
|
58
|
+
export declare function buildControlPlaneActivityQuery(opts: ControlPlaneActivityQueryOpts): string;
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
var s=Object.defineProperty;var o=(e,r)=>s(e,"name",{value:r,configurable:!0});import{MANAGED_USER_NAME_PATTERN as i}from"./clickhouseSqlUsers.js";const E=120;function y(e){const r=e.recencySeconds??E;if(!Number.isInteger(r)||r<=0)throw new Error(`recencySeconds must be a positive integer, got ${String(r)}`);if(e.users.length===0)throw new Error("buildControlPlaneActivityQuery needs at least one user");for(const t of e.users)if(!i.test(t))throw new Error(`Invalid ClickHouse user name for activity probe: ${t}`);const n=e.users.map(t=>`'${t}'`).join(", ");return["SELECT toUInt8(",` (SELECT count() FROM system.processes WHERE user IN (${n})) > 0`," OR",` (SELECT count() FROM system.query_log WHERE user IN (${n})`," AND event_date >= yesterday()"," AND type != 'QueryStart'",` AND event_time >= now() - INTERVAL ${r} SECOND) > 0`,") AS active"].join(`
|
|
2
|
+
`)}o(y,"buildControlPlaneActivityQuery");export{E as CONTROL_PLANE_ACTIVITY_RECENCY_SECONDS,y as buildControlPlaneActivityQuery};
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
export { MIGRATION_SNAPSHOT_NAME_PREFIX, EXPECTED_SCHEMA_VERSION_ENV, EXPECTED_SCHEMA_VERSION_TOOL_ENV, EXPECTED_CH_SCHEMA_VERSION_ENV, SCHEMA_ADMIN_USER_ENV, SCHEMA_ADMIN_PASSWORD_ENV, SCHEMA_GATE_DB_URL_BASE_ENV, SCHEMA_GATE_DB_USER_ENV, SCHEMA_GATE_DB_PASSWORD_ENV, SCHEMA_GATE_CH_URL_ENV, SCHEMA_GATE_CH_DATABASE_ENV, SCHEMA_GATE_CH_CA_CERT_ENV, SCHEMA_GATE_CONTAINER_NAME, SCHEMA_GATE_ECR_REPO_NAME, SCHEMA_GATE_PUBLIC_IMAGE_REPO, PRISMA_MIGRATION_DIR_RE, CLICKHOUSE_MIGRATION_SKIP_RE } from "./constants.js";
|
|
1
|
+
export { MIGRATION_SNAPSHOT_NAME_PREFIX, EXPECTED_SCHEMA_VERSION_ENV, EXPECTED_SCHEMA_VERSION_TOOL_ENV, EXPECTED_CH_SCHEMA_VERSION_ENV, SCHEMA_ADMIN_USER_ENV, SCHEMA_ADMIN_PASSWORD_ENV, SCHEMA_GATE_DB_URL_BASE_ENV, SCHEMA_GATE_DB_USER_ENV, SCHEMA_GATE_DB_PASSWORD_ENV, SCHEMA_GATE_CH_URL_ENV, SCHEMA_GATE_CH_DATABASE_ENV, SCHEMA_GATE_CH_CA_CERT_ENV, SCHEMA_GATE_CH_USER_ENV, SCHEMA_GATE_CH_PASSWORD_ENV, SCHEMA_GATE_START_TIMEOUT_SECONDS, SCHEMA_GATE_CONTAINER_NAME, SCHEMA_GATE_ECR_REPO_NAME, SCHEMA_GATE_PUBLIC_IMAGE_REPO, PRISMA_MIGRATION_DIR_RE, CLICKHOUSE_MIGRATION_SKIP_RE } from "./constants.js";
|
|
2
2
|
export { pickLatestPrismaMigration } from "./pickLatestPrismaMigration.js";
|
|
3
3
|
export { pickLatestClickHouseMigration } from "./pickLatestClickHouseMigration.js";
|
|
4
4
|
export { isOrderableSchemaVersion, isSchemaVersionSatisfied } from "./compareSchemaVersion.js";
|
|
5
5
|
export { type MigrationsSqlClient, type VerifyExpectedSchemaVersionOpts, type VerifyExpectedSchemaVersionResult, verifyExpectedSchemaVersion } from "./verifyExpectedSchemaVersion.js";
|
|
6
6
|
export { CLICKHOUSE_MANAGED_USERS_ENV, MANAGED_USER_NAME_PATTERN, userPasswordEnvName, ManagedUserNameSchema, ManagedUserNamesSchema, type ManagedUserName, type ManagedUserNames } from "./clickhouseSqlUsers.js";
|
|
7
|
+
export { FJALL_SCHEMA_GATE_USER, FJALL_MAINTENANCE_USER, FJALL_CLICKHOUSE_USERS, FJALL_CLICKHOUSE_USER_PREFIX, isReservedClickHouseUserName, CLICKHOUSE_FRAMEWORK_USERS_ENV, CLICKHOUSE_MAINTENANCE_PROFILE_ENV, CLICKHOUSE_BACKUP_SCRATCH_DATABASE, FrameworkUserNameSchema, FrameworkUserNamesSchema, type FjallClickHouseUser, type FrameworkUserName, type FrameworkUserNames } from "./clickhouseFrameworkUsers.js";
|
|
8
|
+
export { SCHEMA_GATE_EXIT, SCHEMA_GATE_EXIT_HINTS, schemaGateExitKind, type SchemaGateExitKind, type SchemaGateExitCode } from "./schemaGateExit.js";
|
|
9
|
+
export { CONTROL_PLANE_ACTIVITY_RECENCY_SECONDS, buildControlPlaneActivityQuery, type ControlPlaneActivityQueryOpts } from "./controlPlaneActivity.js";
|
|
10
|
+
export { sleepAbortable } from "./sleepAbortable.js";
|
package/dist/migration/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{MIGRATION_SNAPSHOT_NAME_PREFIX as A,EXPECTED_SCHEMA_VERSION_ENV as S,EXPECTED_SCHEMA_VERSION_TOOL_ENV as
|
|
1
|
+
import{MIGRATION_SNAPSHOT_NAME_PREFIX as A,EXPECTED_SCHEMA_VERSION_ENV as S,EXPECTED_SCHEMA_VERSION_TOOL_ENV as C,EXPECTED_CH_SCHEMA_VERSION_ENV as e,SCHEMA_ADMIN_USER_ENV as N,SCHEMA_ADMIN_PASSWORD_ENV as r,SCHEMA_GATE_DB_URL_BASE_ENV as T,SCHEMA_GATE_DB_USER_ENV as M,SCHEMA_GATE_DB_PASSWORD_ENV as R,SCHEMA_GATE_CH_URL_ENV as H,SCHEMA_GATE_CH_DATABASE_ENV as I,SCHEMA_GATE_CH_CA_CERT_ENV as o,SCHEMA_GATE_CH_USER_ENV as a,SCHEMA_GATE_CH_PASSWORD_ENV as O,SCHEMA_GATE_START_TIMEOUT_SECONDS as m,SCHEMA_GATE_CONTAINER_NAME as U,SCHEMA_GATE_ECR_REPO_NAME as s,SCHEMA_GATE_PUBLIC_IMAGE_REPO as t,PRISMA_MIGRATION_DIR_RE as L,CLICKHOUSE_MIGRATION_SKIP_RE as i}from"./constants.js";import{pickLatestPrismaMigration as G}from"./pickLatestPrismaMigration.js";import{pickLatestClickHouseMigration as D}from"./pickLatestClickHouseMigration.js";import{isOrderableSchemaVersion as p,isSchemaVersionSatisfied as f}from"./compareSchemaVersion.js";import{verifyExpectedSchemaVersion as n}from"./verifyExpectedSchemaVersion.js";import{CLICKHOUSE_MANAGED_USERS_ENV as F,MANAGED_USER_NAME_PATTERN as d,userPasswordEnvName as h,ManagedUserNameSchema as l,ManagedUserNamesSchema as B}from"./clickhouseSqlUsers.js";import{FJALL_SCHEMA_GATE_USER as k,FJALL_MAINTENANCE_USER as u,FJALL_CLICKHOUSE_USERS as b,FJALL_CLICKHOUSE_USER_PREFIX as g,isReservedClickHouseUserName as v,CLICKHOUSE_FRAMEWORK_USERS_ENV as J,CLICKHOUSE_MAINTENANCE_PROFILE_ENV as W,CLICKHOUSE_BACKUP_SCRATCH_DATABASE as w,FrameworkUserNameSchema as y,FrameworkUserNamesSchema as Y}from"./clickhouseFrameworkUsers.js";import{SCHEMA_GATE_EXIT as j,SCHEMA_GATE_EXIT_HINTS as q,schemaGateExitKind as z}from"./schemaGateExit.js";import{CONTROL_PLANE_ACTIVITY_RECENCY_SECONDS as $,buildControlPlaneActivityQuery as EE}from"./controlPlaneActivity.js";import{sleepAbortable as AE}from"./sleepAbortable.js";export{w as CLICKHOUSE_BACKUP_SCRATCH_DATABASE,J as CLICKHOUSE_FRAMEWORK_USERS_ENV,W as CLICKHOUSE_MAINTENANCE_PROFILE_ENV,F as CLICKHOUSE_MANAGED_USERS_ENV,i as CLICKHOUSE_MIGRATION_SKIP_RE,$ as CONTROL_PLANE_ACTIVITY_RECENCY_SECONDS,e as EXPECTED_CH_SCHEMA_VERSION_ENV,S as EXPECTED_SCHEMA_VERSION_ENV,C as EXPECTED_SCHEMA_VERSION_TOOL_ENV,b as FJALL_CLICKHOUSE_USERS,g as FJALL_CLICKHOUSE_USER_PREFIX,u as FJALL_MAINTENANCE_USER,k as FJALL_SCHEMA_GATE_USER,y as FrameworkUserNameSchema,Y as FrameworkUserNamesSchema,d as MANAGED_USER_NAME_PATTERN,A as MIGRATION_SNAPSHOT_NAME_PREFIX,l as ManagedUserNameSchema,B as ManagedUserNamesSchema,L as PRISMA_MIGRATION_DIR_RE,r as SCHEMA_ADMIN_PASSWORD_ENV,N as SCHEMA_ADMIN_USER_ENV,o as SCHEMA_GATE_CH_CA_CERT_ENV,I as SCHEMA_GATE_CH_DATABASE_ENV,O as SCHEMA_GATE_CH_PASSWORD_ENV,H as SCHEMA_GATE_CH_URL_ENV,a as SCHEMA_GATE_CH_USER_ENV,U as SCHEMA_GATE_CONTAINER_NAME,R as SCHEMA_GATE_DB_PASSWORD_ENV,T as SCHEMA_GATE_DB_URL_BASE_ENV,M as SCHEMA_GATE_DB_USER_ENV,s as SCHEMA_GATE_ECR_REPO_NAME,j as SCHEMA_GATE_EXIT,q as SCHEMA_GATE_EXIT_HINTS,t as SCHEMA_GATE_PUBLIC_IMAGE_REPO,m as SCHEMA_GATE_START_TIMEOUT_SECONDS,EE as buildControlPlaneActivityQuery,p as isOrderableSchemaVersion,v as isReservedClickHouseUserName,f as isSchemaVersionSatisfied,D as pickLatestClickHouseMigration,G as pickLatestPrismaMigration,z as schemaGateExitKind,AE as sleepAbortable,h as userPasswordEnvName,n as verifyExpectedSchemaVersion};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exit taxonomy for the materialised schema-gate container
|
|
3
|
+
* (`@fjall/schema-gate`). The dependent app containers declare
|
|
4
|
+
* `dependsOn: [{ container: "fjall-schema-gate", condition: "SUCCESS" }]`, so
|
|
5
|
+
* ANY non-zero exit stops the task pre-RUNNING and counts toward the
|
|
6
|
+
* deployment circuit breaker — the codes exist for the forensic log and the
|
|
7
|
+
* deploy-time ECS tail, not for ECS itself.
|
|
8
|
+
*
|
|
9
|
+
* Lives in `@fjall/util` because three packages must agree on it: the gate
|
|
10
|
+
* runner emits the codes, `@fjall/deploy-core`'s deployment tail translates
|
|
11
|
+
* them into one human line, and the constructs' docs cite them. Coupled
|
|
12
|
+
* values across package boundaries — a drift would have the tail describe
|
|
13
|
+
* an exit the runner never emits.
|
|
14
|
+
*
|
|
15
|
+
* - `pass` (0) — every configured target verified; app containers start.
|
|
16
|
+
* - `refused` (1) — a target's live schema does not satisfy the expected
|
|
17
|
+
* version (forward skew, or a never-migrated database). The gate did its
|
|
18
|
+
* job; the deploy must roll back or the migration must land first.
|
|
19
|
+
* - `configError` (2) — the gate could not even attempt verification:
|
|
20
|
+
* missing/empty env, unsupported tool or engine family, or an internal
|
|
21
|
+
* error. Points at a construct bug or an author override.
|
|
22
|
+
* - `connectionError` (3) — the server could not be reached after the
|
|
23
|
+
* bounded retry budget (network, TLS, timeout). Indeterminate: the schema
|
|
24
|
+
* may be fine.
|
|
25
|
+
* - `busy` (4) — the server was reachable and answered every attempt, but
|
|
26
|
+
* refused to run the probe because a concurrency cap or quota was
|
|
27
|
+
* exhausted (ClickHouse `TOO_MANY_SIMULTANEOUS_QUERIES` 202 /
|
|
28
|
+
* `QUOTA_EXCEEDED` 201, Postgres `too_many_connections` 53300 /
|
|
29
|
+
* `cannot_connect_now` 57P03) for the whole budget. Indeterminate, and
|
|
30
|
+
* distinct from `connectionError` because the cure is different: the
|
|
31
|
+
* server is healthy and something else holds the slot.
|
|
32
|
+
* - `denied` (5) — the server was reachable and rejected the gate's
|
|
33
|
+
* identity or its rights (auth failure, unknown user, missing grant,
|
|
34
|
+
* read-only violation). Definitive: no retry will change it, and the fix
|
|
35
|
+
* is provisioning, not capacity.
|
|
36
|
+
*
|
|
37
|
+
* Precedence when targets disagree: `configError` is detected before any
|
|
38
|
+
* connection is attempted; then `refused` (definitive evidence), then
|
|
39
|
+
* `denied` (definitive), then `busy`, then `connectionError` (both
|
|
40
|
+
* indeterminate; `busy` carries the more specific cure).
|
|
41
|
+
*/
|
|
42
|
+
export declare const SCHEMA_GATE_EXIT: {
|
|
43
|
+
readonly pass: 0;
|
|
44
|
+
readonly refused: 1;
|
|
45
|
+
readonly configError: 2;
|
|
46
|
+
readonly connectionError: 3;
|
|
47
|
+
readonly busy: 4;
|
|
48
|
+
readonly denied: 5;
|
|
49
|
+
};
|
|
50
|
+
export type SchemaGateExitKind = keyof typeof SCHEMA_GATE_EXIT;
|
|
51
|
+
export type SchemaGateExitCode = (typeof SCHEMA_GATE_EXIT)[SchemaGateExitKind];
|
|
52
|
+
/**
|
|
53
|
+
* One human line per exit kind, for the deploy-time ECS tail and forensic
|
|
54
|
+
* logs. Keyed on the taxonomy so adding an exit kind without a hint is a
|
|
55
|
+
* compile error (`Record<SchemaGateExitKind, string>` rejects a missing key).
|
|
56
|
+
*/
|
|
57
|
+
export declare const SCHEMA_GATE_EXIT_HINTS: Record<SchemaGateExitKind, string>;
|
|
58
|
+
/**
|
|
59
|
+
* Inverse lookup for the deployment tail: an ECS container exit code → the
|
|
60
|
+
* taxonomy kind, or `undefined` for a code the gate never emits (a signal
|
|
61
|
+
* exit such as 143, or an OOM 137).
|
|
62
|
+
*/
|
|
63
|
+
export declare function schemaGateExitKind(code: number): SchemaGateExitKind | undefined;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var o=Object.defineProperty;var t=(e,r)=>o(e,"name",{value:r,configurable:!0});const a={pass:0,refused:1,configError:2,connectionError:3,busy:4,denied:5},i={pass:"schema gate passed",refused:"schema gate refused: live schema does not satisfy the expected version \u2014 land the migration or roll back",configError:"schema gate could not run: missing or invalid gate configuration \u2014 check the task definition's FJALL_SCHEMA_GATE_* env",connectionError:"schema gate could not reach the database within its budget \u2014 check network reachability and database health",busy:"schema gate was refused a query slot for its whole budget \u2014 the database is healthy; another workload holds the concurrency cap or quota",denied:"schema gate identity was rejected \u2014 the gate user's credentials or grants are wrong; re-run the migration task to re-provision it"},s=new Map(Object.keys(a).map(e=>[a[e],e]));function c(e){return s.get(e)}t(c,"schemaGateExitKind");export{a as SCHEMA_GATE_EXIT,i as SCHEMA_GATE_EXIT_HINTS,c as schemaGateExitKind};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Promise-based sleep that short-circuits when the supplied abort signal
|
|
3
|
+
* fires. Poll and retry loops that can be running when a shutdown signal
|
|
4
|
+
* arrives (SIGTERM in a worker, an ECS stop of the gate container) MUST use
|
|
5
|
+
* this for inter-attempt delays so the signal does not stall for the full
|
|
6
|
+
* sleep. Resolves (never rejects) on abort; callers check `signal.aborted`
|
|
7
|
+
* after waking. Shared by `@fjall/clickhouse`, `@fjall/schema-gate` and
|
|
8
|
+
* `@fjall/deploy-core`.
|
|
9
|
+
*/
|
|
10
|
+
export declare function sleepAbortable(ms: number, signal?: AbortSignal): Promise<void>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var u=Object.defineProperty;var r=(t,e)=>u(t,"name",{value:e,configurable:!0});function m(t,e){return e?.aborted===!0?Promise.resolve():new Promise(o=>{const i=setTimeout(()=>{e?.removeEventListener("abort",n),o()},t),n=r(()=>{clearTimeout(i),o()},"onAbort");e?.addEventListener("abort",n,{once:!0})})}r(m,"sleepAbortable");export{m as sleepAbortable};
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The framework vocabulary and its bridge to the pattern vocabulary.
|
|
3
|
+
*
|
|
4
|
+
* Detection answers "what did the author build this with?" (`Framework`).
|
|
5
|
+
* Creation asks "which shape does Fjall deploy?" (`PatternType`). Those are
|
|
6
|
+
* different questions with different membership, and until this module nothing
|
|
7
|
+
* joined them: `apps detect` reported `astro`, the agent skill told the agent to
|
|
8
|
+
* feed what detect reported straight into `--pattern`, and `--pattern astro`
|
|
9
|
+
* reached `PATTERN_REGISTRY[...]` as an unchecked index and threw a `TypeError`
|
|
10
|
+
* that surfaced to the agent as `code: UNKNOWN`. Five of the seven framework
|
|
11
|
+
* values had no pattern at all; the join existed only in the skill's prose.
|
|
12
|
+
*
|
|
13
|
+
* `FRAMEWORK_PATTERN_RECOMMENDATIONS` is the compiler's checklist for that
|
|
14
|
+
* join. It is a `Record<Framework, PatternRecommendation>`, so an eighth
|
|
15
|
+
* framework fails to compile until someone states what it means for
|
|
16
|
+
* `--pattern` — the same discipline `PATTERN_REGISTRY` applies to patterns.
|
|
17
|
+
*
|
|
18
|
+
* The three verdicts are deliberately distinct, because they are three
|
|
19
|
+
* different instructions to hand an agent mid-onboarding:
|
|
20
|
+
*
|
|
21
|
+
* - `recommended` — a deployable pattern fits; proceed with it.
|
|
22
|
+
* - `blocked` — the pattern exists but cannot deploy yet; stop, and say why.
|
|
23
|
+
* - `unsupported` — no pattern shape fits this framework; stop, and say so.
|
|
24
|
+
*
|
|
25
|
+
* `recommended` and `blocked` are separated at the *type* level rather than by
|
|
26
|
+
* author discipline: their `pattern` fields are mapped types filtered on the
|
|
27
|
+
* registry's own `deployable` flag, in the same idiom as `OpenNextPatternType`.
|
|
28
|
+
* Flipping `nextjs` to `deployable: true` makes `UndeployablePatternType`
|
|
29
|
+
* uninhabited and breaks the `blocked` entry, forcing it to be moved rather
|
|
30
|
+
* than left behind as a lie. The reverse holds too: a pattern that loses its
|
|
31
|
+
* construct cannot stay in `recommended`.
|
|
32
|
+
*/
|
|
33
|
+
import { PATTERN_REGISTRY, type PatternType, type PatternCreateInput } from "./patternTypes.js";
|
|
34
|
+
/**
|
|
35
|
+
* Every framework the detector can name.
|
|
36
|
+
*
|
|
37
|
+
* This vocabulary lives here rather than in `@fjall/generator` because the
|
|
38
|
+
* bridge below needs both halves in one place, and `generator` already depends
|
|
39
|
+
* on `@fjall/util` (never the reverse). `generator/src/detection` re-exports
|
|
40
|
+
* these names and builds the Zod schema over them, exactly as it already does
|
|
41
|
+
* for `STATIC_SITE_ROUTING_VALUES`.
|
|
42
|
+
*
|
|
43
|
+
* - `nextjs+payload` is a strict superset of both its parts and must be
|
|
44
|
+
* matched before either: Payload v3 ships as a Next.js plugin, so both
|
|
45
|
+
* dependencies appear in a Payload app.
|
|
46
|
+
* - `unknown` is a real member, not an absence — a repository with a parseable
|
|
47
|
+
* `package.json` and no recognised signal is still a detection result.
|
|
48
|
+
*/
|
|
49
|
+
export declare const FRAMEWORK_VALUES: readonly ["nextjs", "payload", "nextjs+payload", "express", "remix", "astro", "unknown"];
|
|
50
|
+
export type Framework = (typeof FRAMEWORK_VALUES)[number];
|
|
51
|
+
export declare const FRAMEWORKS: ReadonlySet<string>;
|
|
52
|
+
export declare function isFramework(value: unknown): value is Framework;
|
|
53
|
+
/**
|
|
54
|
+
* The patterns that can deploy end-to-end, derived from the registry's own
|
|
55
|
+
* flag at the type level. `DEPLOYABLE_PATTERN_TYPES` is the value-level twin.
|
|
56
|
+
*/
|
|
57
|
+
export type DeployablePatternType = {
|
|
58
|
+
[K in PatternType]: (typeof PATTERN_REGISTRY)[K]["deployable"] extends true ? K : never;
|
|
59
|
+
}[PatternType];
|
|
60
|
+
/**
|
|
61
|
+
* The patterns the generator knows but cannot yet synthesise. Uninhabited once
|
|
62
|
+
* every pattern is deployable — at which point the `blocked` verdict below
|
|
63
|
+
* stops compiling, which is the intended signal, not a defect.
|
|
64
|
+
*/
|
|
65
|
+
export type UndeployablePatternType = {
|
|
66
|
+
[K in PatternType]: (typeof PATTERN_REGISTRY)[K]["deployable"] extends false ? K : never;
|
|
67
|
+
}[PatternType];
|
|
68
|
+
/**
|
|
69
|
+
* What a detected framework means for `--pattern`.
|
|
70
|
+
*
|
|
71
|
+
* Every arm carries prose because every arm is read aloud to a user by an
|
|
72
|
+
* agent. A verdict an agent cannot explain is a verdict it will second-guess,
|
|
73
|
+
* and second-guessing is what the "do not infer the pattern" instruction in the
|
|
74
|
+
* skill exists to prevent.
|
|
75
|
+
*/
|
|
76
|
+
export type PatternRecommendation = {
|
|
77
|
+
readonly kind: "recommended";
|
|
78
|
+
/** Constrained to deployable patterns by the registry's own flag. */
|
|
79
|
+
readonly pattern: DeployablePatternType;
|
|
80
|
+
/** Why this pattern fits, in the agent's own words to the user. */
|
|
81
|
+
readonly rationale: string;
|
|
82
|
+
} | {
|
|
83
|
+
readonly kind: "blocked";
|
|
84
|
+
/** Constrained to patterns the registry marks undeployable. */
|
|
85
|
+
readonly pattern: UndeployablePatternType;
|
|
86
|
+
/** What is missing, so the agent can say more than "no". */
|
|
87
|
+
readonly reason: string;
|
|
88
|
+
} | {
|
|
89
|
+
readonly kind: "unsupported";
|
|
90
|
+
/** Why no pattern fits this framework's shape. */
|
|
91
|
+
readonly reason: string;
|
|
92
|
+
};
|
|
93
|
+
/**
|
|
94
|
+
* The framework → pattern join.
|
|
95
|
+
*
|
|
96
|
+
* This is the *evidence-free* verdict: it knows only the framework name. A
|
|
97
|
+
* detector holding more evidence (an Astro config declaring `output: "server"`,
|
|
98
|
+
* say) returns the same union with a different arm — that is a refinement of
|
|
99
|
+
* one contract, not a second framing path.
|
|
100
|
+
*/
|
|
101
|
+
export declare const FRAMEWORK_PATTERN_RECOMMENDATIONS: {
|
|
102
|
+
readonly astro: {
|
|
103
|
+
readonly kind: "recommended";
|
|
104
|
+
readonly pattern: "staticsite";
|
|
105
|
+
readonly rationale: "Astro builds to a directory of static assets, which is what the static-site pattern serves from S3 behind CloudFront.";
|
|
106
|
+
};
|
|
107
|
+
readonly payload: {
|
|
108
|
+
readonly kind: "recommended";
|
|
109
|
+
readonly pattern: "payload";
|
|
110
|
+
readonly rationale: "Payload CMS runs on OpenNext with a database and migrations, which the payload pattern provisions.";
|
|
111
|
+
};
|
|
112
|
+
readonly "nextjs+payload": {
|
|
113
|
+
readonly kind: "recommended";
|
|
114
|
+
readonly pattern: "payload";
|
|
115
|
+
readonly rationale: "Payload v3 ships as a Next.js plugin, so a repository carrying both dependencies is a Payload application and deploys as one.";
|
|
116
|
+
};
|
|
117
|
+
readonly nextjs: {
|
|
118
|
+
readonly kind: "blocked";
|
|
119
|
+
readonly pattern: "nextjs";
|
|
120
|
+
readonly reason: "The Next.js pattern has no CDK construct yet — IPatternProps omits it, so a generated app fails at synthesis. Deploy a Payload application, or supply a static export with --pattern staticsite.";
|
|
121
|
+
};
|
|
122
|
+
readonly remix: {
|
|
123
|
+
readonly kind: "unsupported";
|
|
124
|
+
readonly reason: "Remix serves from a Node server at request time; Fjall has no server-rendering pattern beyond OpenNext, and no static-export path from Remix.";
|
|
125
|
+
};
|
|
126
|
+
readonly express: {
|
|
127
|
+
readonly kind: "unsupported";
|
|
128
|
+
readonly reason: "Express is a long-running HTTP server. Fjall's patterns cover OpenNext applications and pre-built static sites; neither shape fits.";
|
|
129
|
+
};
|
|
130
|
+
readonly unknown: {
|
|
131
|
+
readonly kind: "unsupported";
|
|
132
|
+
readonly reason: "No framework signal was found in package.json. If this is a pre-built static site, pass --pattern staticsite explicitly with --source, --build-command and --output-dir.";
|
|
133
|
+
};
|
|
134
|
+
};
|
|
135
|
+
/**
|
|
136
|
+
* The evidence-free verdict for a framework. Total by construction — there is
|
|
137
|
+
* no undefined branch to guard, which is the whole point of the record above.
|
|
138
|
+
*/
|
|
139
|
+
export declare function recommendPatternForFramework(framework: Framework): PatternRecommendation;
|
|
140
|
+
/**
|
|
141
|
+
* The frameworks an agent can carry all the way to a deploy today, derived
|
|
142
|
+
* from the table rather than restated. Used by the skill's vocabulary block so
|
|
143
|
+
* its prose cannot drift from the join.
|
|
144
|
+
*/
|
|
145
|
+
export declare const CREATABLE_FRAMEWORKS: readonly Framework[];
|
|
146
|
+
/**
|
|
147
|
+
* The inputs a framework's recommended pattern still needs from the caller.
|
|
148
|
+
* Empty for anything not `recommended` — there is nothing to collect for a
|
|
149
|
+
* pattern that cannot be created.
|
|
150
|
+
*/
|
|
151
|
+
export declare function requiredInputsForFramework(framework: Framework): readonly PatternCreateInput[];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var o=Object.defineProperty;var a=(e,t)=>o(e,"name",{value:t,configurable:!0});import{PATTERN_REGISTRY as s}from"./patternTypes.js";const r=["nextjs","payload","nextjs+payload","express","remix","astro","unknown"],i=new Set(r);function c(e){return typeof e=="string"&&i.has(e)}a(c,"isFramework");const n={astro:{kind:"recommended",pattern:"staticsite",rationale:"Astro builds to a directory of static assets, which is what the static-site pattern serves from S3 behind CloudFront."},payload:{kind:"recommended",pattern:"payload",rationale:"Payload CMS runs on OpenNext with a database and migrations, which the payload pattern provisions."},"nextjs+payload":{kind:"recommended",pattern:"payload",rationale:"Payload v3 ships as a Next.js plugin, so a repository carrying both dependencies is a Payload application and deploys as one."},nextjs:{kind:"blocked",pattern:"nextjs",reason:"The Next.js pattern has no CDK construct yet \u2014 IPatternProps omits it, so a generated app fails at synthesis. Deploy a Payload application, or supply a static export with --pattern staticsite."},remix:{kind:"unsupported",reason:"Remix serves from a Node server at request time; Fjall has no server-rendering pattern beyond OpenNext, and no static-export path from Remix."},express:{kind:"unsupported",reason:"Express is a long-running HTTP server. Fjall's patterns cover OpenNext applications and pre-built static sites; neither shape fits."},unknown:{kind:"unsupported",reason:"No framework signal was found in package.json. If this is a pre-built static site, pass --pattern staticsite explicitly with --source, --build-command and --output-dir."}};function l(e){return n[e]}a(l,"recommendPatternForFramework");const m=r.filter(e=>n[e].kind==="recommended");function u(e){const t=n[e];return t.kind==="recommended"?s[t.pattern].requiredCreateInputs:[]}a(u,"requiredInputsForFramework");export{m as CREATABLE_FRAMEWORKS,i as FRAMEWORKS,n as FRAMEWORK_PATTERN_RECOMMENDATIONS,r as FRAMEWORK_VALUES,c as isFramework,l as recommendPatternForFramework,u as requiredInputsForFramework};
|
package/dist/patterns/index.d.ts
CHANGED
|
@@ -7,5 +7,6 @@
|
|
|
7
7
|
* the webapp bundles) can take the vocabulary without depending on the root
|
|
8
8
|
* barrel's contents staying pure.
|
|
9
9
|
*/
|
|
10
|
-
export { PATTERN_TYPE_VALUES, type PatternType, PATTERN_TYPES, isPatternType, type PatternArtefact, type PatternStackPlacement, type PatternDescriptor, PATTERN_REGISTRY, patternConstructId, DEPLOYABLE_PATTERN_TYPES, type OpenNextPatternType, OPENNEXT_PATTERN_TYPES, isOpenNextPatternType, STATIC_SITE_ROUTING_VALUES, type StaticSiteRouting } from "./patternTypes.js";
|
|
10
|
+
export { PATTERN_TYPE_VALUES, type PatternType, PATTERN_TYPES, isPatternType, type PatternArtefact, type PatternStackPlacement, type PatternDescriptor, PATTERN_REGISTRY, patternConstructId, DEPLOYABLE_PATTERN_TYPES, type OpenNextPatternType, OPENNEXT_PATTERN_TYPES, isOpenNextPatternType, STATIC_SITE_ROUTING_VALUES, type StaticSiteRouting, PATTERN_CREATE_INPUT_VALUES, type PatternCreateInput, PATTERN_CREATE_INPUT_FLAGS } from "./patternTypes.js";
|
|
11
|
+
export { FRAMEWORK_VALUES, type Framework, FRAMEWORKS, isFramework, type DeployablePatternType, type UndeployablePatternType, type PatternRecommendation, FRAMEWORK_PATTERN_RECOMMENDATIONS, recommendPatternForFramework, CREATABLE_FRAMEWORKS, requiredInputsForFramework } from "./frameworkPatterns.js";
|
|
11
12
|
export { DEFAULT_FORMS_FROM_LOCAL_PART, defaultFormsFromAddress, defaultFormsCorsOrigin, isAddressAtDomain } from "./staticSiteForms.js";
|
package/dist/patterns/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{PATTERN_TYPE_VALUES as
|
|
1
|
+
import{PATTERN_TYPE_VALUES as A,PATTERN_TYPES as r,isPatternType as R,PATTERN_REGISTRY as _,patternConstructId as e,DEPLOYABLE_PATTERN_TYPES as P,OPENNEXT_PATTERN_TYPES as o,isOpenNextPatternType as t,STATIC_SITE_ROUTING_VALUES as F,PATTERN_CREATE_INPUT_VALUES as N,PATTERN_CREATE_INPUT_FLAGS as S}from"./patternTypes.js";import{FRAMEWORK_VALUES as s,FRAMEWORKS as m,isFramework as L,FRAMEWORK_PATTERN_RECOMMENDATIONS as a,recommendPatternForFramework as n,CREATABLE_FRAMEWORKS as d,requiredInputsForFramework as I}from"./frameworkPatterns.js";import{DEFAULT_FORMS_FROM_LOCAL_PART as p,defaultFormsFromAddress as C,defaultFormsCorsOrigin as M,isAddressAtDomain as U}from"./staticSiteForms.js";export{d as CREATABLE_FRAMEWORKS,p as DEFAULT_FORMS_FROM_LOCAL_PART,P as DEPLOYABLE_PATTERN_TYPES,m as FRAMEWORKS,a as FRAMEWORK_PATTERN_RECOMMENDATIONS,s as FRAMEWORK_VALUES,o as OPENNEXT_PATTERN_TYPES,S as PATTERN_CREATE_INPUT_FLAGS,N as PATTERN_CREATE_INPUT_VALUES,_ as PATTERN_REGISTRY,r as PATTERN_TYPES,A as PATTERN_TYPE_VALUES,F as STATIC_SITE_ROUTING_VALUES,M as defaultFormsCorsOrigin,C as defaultFormsFromAddress,U as isAddressAtDomain,L as isFramework,t as isOpenNextPatternType,R as isPatternType,e as patternConstructId,n as recommendPatternForFramework,I as requiredInputsForFramework};
|
|
@@ -31,6 +31,26 @@ export type PatternArtefact = "opennext-lambda" | "static-assets";
|
|
|
31
31
|
* Lambda (and usually a database) and belong in the compute stack.
|
|
32
32
|
*/
|
|
33
33
|
export type PatternStackPlacement = "compute" | "cdn";
|
|
34
|
+
/**
|
|
35
|
+
* Create-time inputs a pattern cannot invent for itself.
|
|
36
|
+
*
|
|
37
|
+
* A static site is built from someone else's repository, so Fjall cannot guess
|
|
38
|
+
* where the sources live, how they build, or which directory to upload — get
|
|
39
|
+
* `outputDir` wrong and the deploy succeeds onto an empty bucket. Declaring the
|
|
40
|
+
* set here rather than in the create handler's control flow is what lets the
|
|
41
|
+
* CLI refuse the command with a list of missing flags instead of surfacing a
|
|
42
|
+
* raw Zod issue array, and lets `apps detect` prefill exactly the values the
|
|
43
|
+
* chosen pattern will be asked for.
|
|
44
|
+
*/
|
|
45
|
+
export declare const PATTERN_CREATE_INPUT_VALUES: readonly ["source", "buildCommand", "outputDir"];
|
|
46
|
+
export type PatternCreateInput = (typeof PATTERN_CREATE_INPUT_VALUES)[number];
|
|
47
|
+
/** The CLI flag each input arrives on. Kept beside the vocabulary so error
|
|
48
|
+
* messages name a flag the user can actually type. */
|
|
49
|
+
export declare const PATTERN_CREATE_INPUT_FLAGS: {
|
|
50
|
+
readonly source: "--source";
|
|
51
|
+
readonly buildCommand: "--build-command";
|
|
52
|
+
readonly outputDir: "--output-dir";
|
|
53
|
+
};
|
|
34
54
|
export interface PatternDescriptor {
|
|
35
55
|
/** Human-facing name, used by the CLI picker and progress output. */
|
|
36
56
|
readonly label: string;
|
|
@@ -48,6 +68,12 @@ export interface PatternDescriptor {
|
|
|
48
68
|
* adding the construct.
|
|
49
69
|
*/
|
|
50
70
|
readonly deployable: boolean;
|
|
71
|
+
/**
|
|
72
|
+
* Inputs the caller must supply for this pattern; the create flow refuses
|
|
73
|
+
* the command when any are absent. Empty for patterns whose scaffold is
|
|
74
|
+
* generated wholesale and therefore knows its own layout.
|
|
75
|
+
*/
|
|
76
|
+
readonly requiredCreateInputs: readonly PatternCreateInput[];
|
|
51
77
|
}
|
|
52
78
|
export declare const PATTERN_REGISTRY: {
|
|
53
79
|
readonly payload: {
|
|
@@ -56,6 +82,7 @@ export declare const PATTERN_REGISTRY: {
|
|
|
56
82
|
readonly artefact: "opennext-lambda";
|
|
57
83
|
readonly stackPlacement: "compute";
|
|
58
84
|
readonly deployable: true;
|
|
85
|
+
readonly requiredCreateInputs: readonly [];
|
|
59
86
|
};
|
|
60
87
|
readonly nextjs: {
|
|
61
88
|
readonly label: "Next.js";
|
|
@@ -63,6 +90,7 @@ export declare const PATTERN_REGISTRY: {
|
|
|
63
90
|
readonly artefact: "opennext-lambda";
|
|
64
91
|
readonly stackPlacement: "compute";
|
|
65
92
|
readonly deployable: false;
|
|
93
|
+
readonly requiredCreateInputs: readonly [];
|
|
66
94
|
};
|
|
67
95
|
readonly staticsite: {
|
|
68
96
|
readonly label: "Static site";
|
|
@@ -70,6 +98,7 @@ export declare const PATTERN_REGISTRY: {
|
|
|
70
98
|
readonly artefact: "static-assets";
|
|
71
99
|
readonly stackPlacement: "cdn";
|
|
72
100
|
readonly deployable: true;
|
|
101
|
+
readonly requiredCreateInputs: readonly ["source", "buildCommand", "outputDir"];
|
|
73
102
|
};
|
|
74
103
|
};
|
|
75
104
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
var
|
|
1
|
+
var n=Object.defineProperty;var e=(t,o)=>n(t,"name",{value:o,configurable:!0});import{toPascalCase as s}from"../naming/caseConversion.js";const a=["payload","nextjs","staticsite"],c=new Set(a);function l(t){return typeof t=="string"&&c.has(t)}e(l,"isPatternType");const d=["source","buildCommand","outputDir"],E={source:"--source",buildCommand:"--build-command",outputDir:"--output-dir"},r={payload:{label:"Payload CMS",constructIdSuffix:"Payload",artefact:"opennext-lambda",stackPlacement:"compute",deployable:!0,requiredCreateInputs:[]},nextjs:{label:"Next.js",constructIdSuffix:"Nextjs",artefact:"opennext-lambda",stackPlacement:"compute",deployable:!1,requiredCreateInputs:[]},staticsite:{label:"Static site",constructIdSuffix:"StaticSite",artefact:"static-assets",stackPlacement:"cdn",deployable:!0,requiredCreateInputs:["source","buildCommand","outputDir"]}};function P(t,o){return`${s(t)}${r[o].constructIdSuffix}`}e(P,"patternConstructId");const x=a.filter(t=>r[t].deployable),u=a.filter(t=>r[t].artefact==="opennext-lambda"),p=new Set(u);function f(t){return t!=null&&p.has(t)}e(f,"isOpenNextPatternType");const S=["multipage","spa"];export{x as DEPLOYABLE_PATTERN_TYPES,u as OPENNEXT_PATTERN_TYPES,E as PATTERN_CREATE_INPUT_FLAGS,d as PATTERN_CREATE_INPUT_VALUES,r as PATTERN_REGISTRY,c as PATTERN_TYPES,a as PATTERN_TYPE_VALUES,S as STATIC_SITE_ROUTING_VALUES,f as isOpenNextPatternType,l as isPatternType,P as patternConstructId};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Idempotently guarantee that a `.gitignore` in `directory` carries `entries`.
|
|
3
|
+
*
|
|
4
|
+
* Absent file → written with `header` followed by the entries. Existing file →
|
|
5
|
+
* only the missing entries are appended, under `marker` so the addition is
|
|
6
|
+
* attributable. An entry already present anywhere in the file (at any
|
|
7
|
+
* indentation) is left alone, so repeat calls are no-ops.
|
|
8
|
+
*
|
|
9
|
+
* Best-effort by design: this maintains repository hygiene, never correctness.
|
|
10
|
+
* The caller's real work — writing config or recording deploy state — must not
|
|
11
|
+
* fail because a `.gitignore` could not be written (read-only checkout, no
|
|
12
|
+
* repository at all, permissions). Returns whether the file now provably
|
|
13
|
+
* carries every entry, for callers that want to assert or log.
|
|
14
|
+
*/
|
|
15
|
+
export declare function ensureGitignoreEntries(directory: string, entries: readonly string[], options: {
|
|
16
|
+
header?: string;
|
|
17
|
+
marker: string;
|
|
18
|
+
}): boolean;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
var d=Object.defineProperty;var u=(o,e)=>d(o,"name",{value:e,configurable:!0});import i from"fs";import g from"path";function l(o,e,s){const t=g.join(o,".gitignore");try{const r=i.existsSync(t)?i.readFileSync(t,"utf8"):void 0;if(r===void 0){const n=s.header??"";return i.writeFileSync(t,`${n}${e.join(`
|
|
2
|
+
`)}
|
|
3
|
+
`,{encoding:"utf8"}),!0}const a=new Set(r.split(`
|
|
4
|
+
`).map(n=>n.trim())),c=e.filter(n=>!a.has(n));if(c.length===0)return!0;const f=r===""||r.endsWith(`
|
|
5
|
+
`)?"":`
|
|
6
|
+
`;return i.appendFileSync(t,`${f}${s.marker}
|
|
7
|
+
${c.join(`
|
|
8
|
+
`)}
|
|
9
|
+
`,"utf8"),!0}catch{return!1}}u(l,"ensureGitignoreEntries");export{l as ensureGitignoreEntries};
|