@suluk/platform 0.4.2 → 0.5.1

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/package.json CHANGED
@@ -1,35 +1,39 @@
1
1
  {
2
2
  "name": "@suluk/platform",
3
- "version": "0.4.2",
3
+ "version": "0.5.1",
4
4
  "description": "The platform generator (C051): write one `definePlatform` manifest → it plans the shadcn-registry adds, generates the wired Hono entry, and merges each module's provision fragment into a single provision.config. The manifest compiles to a shadcn-add list + a C047 provision.config; the generator runs the adds + `@suluk/provision`. Turns the Suluk backend registry into a one-command platform. CANDIDATE tooling.",
5
5
  "publishConfig": {
6
- "access": "public"
7
- },
8
- "license": "Apache-2.0",
9
- "repository": {
10
- "type": "git",
11
- "url": "git+https://github.com/MahmoodKhalil57/suluk.git",
12
- "directory": "tooling/ts/packages/platform"
13
- },
14
- "homepage": "https://github.com/MahmoodKhalil57/suluk#readme",
15
- "bugs": "https://github.com/MahmoodKhalil57/suluk/issues",
16
- "type": "module",
17
- "files": ["src", "bin", "README.md"],
18
- "main": "src/index.ts",
19
- "bin": {
20
- "suluk-platform": "bin/platform.ts"
21
- },
22
- "exports": {
23
- ".": "./src/index.ts"
24
- },
25
- "dependencies": {
26
- "@suluk/provision": "^0.1.0"
27
- },
28
- "devDependencies": {
29
- "@types/bun": "latest"
30
- },
31
- "scripts": {
32
- "test": "bun test",
33
- "typecheck": "tsc --noEmit -p ."
34
- }
6
+ "access": "public"
7
+ },
8
+ "license": "Apache-2.0",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/MahmoodKhalil57/suluk.git",
12
+ "directory": "tooling/ts/packages/platform"
13
+ },
14
+ "homepage": "https://github.com/MahmoodKhalil57/suluk#readme",
15
+ "bugs": "https://github.com/MahmoodKhalil57/suluk/issues",
16
+ "type": "module",
17
+ "files": [
18
+ "src",
19
+ "bin",
20
+ "README.md"
21
+ ],
22
+ "main": "src/index.ts",
23
+ "bin": {
24
+ "suluk-platform": "bin/platform.ts"
25
+ },
26
+ "exports": {
27
+ ".": "./src/index.ts"
28
+ },
29
+ "dependencies": {
30
+ "@suluk/provision": "^0.1.0"
31
+ },
32
+ "devDependencies": {
33
+ "@types/bun": "latest"
34
+ },
35
+ "scripts": {
36
+ "test": "bun test",
37
+ "typecheck": "tsc --noEmit -p ."
38
+ }
35
39
  }
package/src/generate.ts CHANGED
@@ -85,6 +85,12 @@ export async function generatePlatform(input: PlatformManifest | Platform, opts:
85
85
  log("▸ writing provision.config.ts");
86
86
  await opts.write("provision.config.ts", plan.provisionConfig);
87
87
  written.push("src/index.ts", "provision.config.ts");
88
+ // the bun MOCK-PROVIDER dev server — only when the manifest sets `local: true` (else undefined → not written).
89
+ if (plan.devEntry) {
90
+ log("▸ writing src/dev.ts");
91
+ await opts.write("src/dev.ts", plan.devEntry);
92
+ written.push("src/dev.ts");
93
+ }
88
94
 
89
95
  log(`✓ generated ${name}: ${plan.services.length} services. Next: bun install && suluk-provision apply`);
90
96
  return { plan, added, written };
package/src/manifest.ts CHANGED
@@ -22,6 +22,10 @@ export interface PlatformManifest {
22
22
  opts?: Record<string, Record<string, unknown>>;
23
23
  /** NON-SECRET config values → generated into `wrangler.toml` `[vars]`. SECRETS never go here (they live in `.env`). */
24
24
  vars?: Record<string, string>;
25
+ /** emit the MOCK-PROVIDER dev runtime: a `src/dev.ts` that runs the app under bun with a bun:sqlite DB + JSON-file KV +
26
+ * mocked providers when their keys are absent (mock-until-keyed), and the `dev` script pointed at it. Default false →
27
+ * the scaffold is byte-for-byte the C051 golden. */
28
+ local?: boolean;
25
29
  }
26
30
 
27
31
  // ── C053: the open system/brand surface ──────────────────────────────────────────────────────────────────────────────
@@ -62,6 +66,9 @@ export interface SystemManifest<T extends readonly ServiceRef[] = readonly Servi
62
66
  serviceOpts?: Partial<{ [K in T[number] as IdOf<K>]: SoOf<K> }>;
63
67
  /** inter-service composition edges (Phase 3). */
64
68
  wire?: WireDecl[];
69
+ /** emit the MOCK-PROVIDER dev runtime (a `src/dev.ts` bun server with a bun:sqlite DB + JSON KV + mocked providers when
70
+ * keys are absent). A SYSTEM-level property (the app structure), swappable per brand only if a brand overrides it. */
71
+ local?: boolean;
65
72
  }
66
73
 
67
74
  /** A BRAND — thin, swappable per deployment. Carries the app identity + the brand-facing opts (→ `[vars]`). */
package/src/plan.ts CHANGED
@@ -49,6 +49,9 @@ export interface PlatformPlan {
49
49
  /** the generated `.env` SCAFFOLD (committed) — a header + the setup steps, NO values. `generate` writes it only if absent
50
50
  * (never clobbering the operator's encrypted secrets). Secret VALUES are added encrypted via `suluk-env set`. */
51
51
  envScaffold: string;
52
+ /** the generated `src/dev.ts` — the bun MOCK-PROVIDER dev server (bun:sqlite DB + JSON KV + mocked providers when keys
53
+ * absent). Present ONLY when the manifest sets `local: true`; undefined otherwise (so the golden path is unchanged). */
54
+ devEntry?: string;
52
55
  }
53
56
 
54
57
  export function planPlatform(input: PlatformManifest | Platform): PlatformPlan {
@@ -66,18 +69,20 @@ export function planPlatform(input: PlatformManifest | Platform): PlatformPlan {
66
69
  const env = collectEnv(services, catalog);
67
70
  // resolve the wires (a `{system,brand}` platform may carry `wire`; a legacy manifest never does → no wiring → byte-identical).
68
71
  const wiring = resolveWiring(services, isPlatform(input) ? input.system.wire ?? [] : [], catalog);
72
+ // the MOCK-PROVIDER dev runtime is OPT-IN (`local: true`); off → every output below is byte-for-byte the C051 golden.
73
+ const local = manifest.local === true;
69
74
  return {
70
75
  services,
71
76
  // a service may override the registry it's pulled from (multi-registry); core services fall back to the system registry.
72
77
  adds: services.map((s) => `${catalog[s].registry ?? manifest.registry}/${s}`),
73
- entry: buildEntry(services, manifest.opts, wiring, catalog),
78
+ entry: buildEntry(services, manifest.opts, wiring, catalog, local),
74
79
  provisionConfig: buildProvisionConfig(services, catalog),
75
- packageJson: buildPackageJson(manifest.name, services, catalog),
80
+ packageJson: buildPackageJson(manifest.name, services, catalog, local),
76
81
  tsconfig: buildTsconfig(),
77
82
  componentsJson: buildComponentsJson(),
78
83
  envExample: buildEnvExample(env),
79
84
  wranglerToml: buildWranglerToml(manifest.name, services, env, manifest.vars ?? {}),
80
- gitignore: buildGitignore(),
85
+ gitignore: buildGitignore(local),
81
86
  envCheck: buildEnvCheckScript(env),
82
87
  envTs: buildEnvTs(env),
83
88
  syncSecrets: buildSyncSecrets(),
@@ -86,6 +91,7 @@ export function planPlatform(input: PlatformManifest | Platform): PlatformPlan {
86
91
  provisionScript: buildProvisionScript(env),
87
92
  mintTokens: buildMintTokens(env),
88
93
  envScaffold: buildEnvScaffold(env),
94
+ ...(local ? { devEntry: buildDevEntry(services) } : {}),
89
95
  };
90
96
  }
91
97
 
@@ -445,10 +451,11 @@ export function mergeWranglerToml(generated: string, existing: string | null): s
445
451
  .join("\n");
446
452
  }
447
453
 
448
- function buildGitignore(): string {
454
+ function buildGitignore(local = false): string {
449
455
  // NOTE: `.env` is NOT ignored — it is COMMITTED with its secret values ENCRYPTED (@suluk/env). The PRIVATE key
450
456
  // (`.env.keys`) is what must never be committed; that + `.env.temp`/`.dev.vars` are ignored.
451
- return ["node_modules/", ".env.keys", ".env.temp", ".dev.vars", ".wrangler/", "dist/", ""].join("\n");
457
+ // local mode also ignores `.suluk/` (the bun-dev sqlite DB + JSON KV — local dev data, never committed).
458
+ return ["node_modules/", ".env.keys", ".env.temp", ".dev.vars", ".wrangler/", "dist/", ...(local ? [".suluk/"] : []), ""].join("\n");
452
459
  }
453
460
 
454
461
  /** Merge the generated .gitignore into an existing one — APPEND any missing entries (never skip-if-present, so an app's
@@ -514,7 +521,7 @@ process.exit(0);
514
521
 
515
522
  /** The framework baseline package.json — name from the manifest, the union of BASE + each service's deps (versions
516
523
  * resolved: @suluk/* → "latest", ecosystem → pinned), + the toolchain devDeps + the regenerate/typecheck scripts. */
517
- export function buildPackageJson(name: string, services: string[], catalog: Record<string, Service> = CORE_SERVICES): string {
524
+ export function buildPackageJson(name: string, services: string[], catalog: Record<string, Service> = CORE_SERVICES, local = false): string {
518
525
  const deps = new Set<string>(BASE_DEPS);
519
526
  for (const s of services) for (const d of catalog[s]?.deps ?? []) deps.add(d);
520
527
  const dependencies: Record<string, string> = {};
@@ -526,8 +533,10 @@ export function buildPackageJson(name: string, services: string[], catalog: Reco
526
533
  scripts: {
527
534
  generate: "suluk-platform", // re-pull modules + rewrite the scaffold config + src/index.ts + provision.config.ts
528
535
  check: "bun run scripts/env-check.ts", // the encrypted-env preflight (keypair present? required secrets set + encrypted?)
529
- predev: "bun run scripts/env-check.ts", // runs automatically before `dev`
530
- dev: "wrangler dev",
536
+ // local mode: `dev` runs the bun mock-provider server (no keys needed → no env-check predev); `dev:cf` keeps wrangler.
537
+ ...(local ? {} : { predev: "bun run scripts/env-check.ts" }), // (non-local) runs automatically before `dev`
538
+ dev: local ? "bun run --hot src/dev.ts" : "wrangler dev",
539
+ ...(local ? { "dev:cf": "wrangler dev" } : {}),
531
540
  deploy: "wrangler deploy",
532
541
  "env:keygen": "suluk-env keygen", // create the @suluk/env keypair (SULUK_PUBLIC_KEY → .env; private → .env.keys)
533
542
  "link-key": "bun run scripts/link-key.ts", // register the private key in ~/.suluk/settings.json (the central store)
@@ -608,7 +617,7 @@ function buildComponentsJson(): string {
608
617
  );
609
618
  }
610
619
 
611
- function buildEntry(services: string[], opts?: Record<string, Record<string, unknown>>, wiring?: Wiring, catalog: Record<string, Service> = CORE_SERVICES): string {
620
+ function buildEntry(services: string[], opts?: Record<string, Record<string, unknown>>, wiring?: Wiring, catalog: Record<string, Service> = CORE_SERVICES, local = false): string {
612
621
  const imports = [
613
622
  'import { createApp } from "./app";',
614
623
  'import { loadEnv } from "@suluk/env";',
@@ -657,7 +666,10 @@ function buildEntry(services: string[], opts?: Record<string, Record<string, unk
657
666
  return false; // same symbol + module already imported → dedup
658
667
  });
659
668
  for (const line of groupImports(safeWireImports)) imports.push(line);
660
- const body = ["const app = createApp();", ...middleware, ...routes];
669
+ // local mode also EXPORTS the wired app so `src/dev.ts` can serve it under bun with mock bindings (the Worker `fetch`
670
+ // export is unchanged). Off → `const app` (byte-identical to the golden). The mock modules are imported ONLY by dev.ts,
671
+ // so `wrangler deploy` (bundling from `src/index.ts`) never pulls bun:sqlite into the Worker.
672
+ const body = [`${local ? "export const app" : "const app"} = createApp();`, ...middleware, ...routes];
661
673
  // the @suluk/env bootstrap: the committed .env holds the app's secrets ENCRYPTED. If SULUK_PRIVATE_KEY is set (a wrangler
662
674
  // secret), decrypt them into the request env on first use (the runtime path); otherwise this is a no-op and the secrets come
663
675
  // from `wrangler secret put` (the `bun run sync-secrets` deploy path). Decrypt once per isolate (env is stable across requests).
@@ -674,6 +686,65 @@ function buildEntry(services: string[], opts?: Record<string, Record<string, unk
674
686
  return `// AUTO-GENERATED by @suluk/platform from platform.config.ts — the wired Hono entry. Edit freely.\n${imports.join("\n")}\n\n${body.join("\n")}\n\n${bootstrap.join("\n")}\n`;
675
687
  }
676
688
 
689
+ /**
690
+ * `src/dev.ts` — the bun MOCK-PROVIDER dev server (emitted only when `local: true`). Runs the SAME wired app (imported from
691
+ * `src/index.ts`) under bun with a bun:sqlite `DB` facade + a JSON-file KV, so `bun run dev` needs no Cloudflare account and
692
+ * no wrangler. Mock-until-keyed: it decrypts the committed `.env` if the app has been provisioned (real HTTP providers), else
693
+ * every provider falls to its module's mock. The deployed Worker (`src/index.ts`) imports NONE of this — bun:sqlite stays out.
694
+ */
695
+ function buildDevEntry(services: string[]): string {
696
+ const usesKv = services.includes("rate-credit");
697
+ const usesEmail = services.includes("email");
698
+ const usesBilling = services.includes("billing");
699
+ const localImports = ["d1FromSqlite", ...(usesKv ? ["jsonFileKvStore"] : []), ...(usesEmail ? ["jsonFileMailbox"] : []), "applyLocalSchema"];
700
+ const billingImport = usesBilling ? '\nimport { mockStripeFetch } from "@suluk/billing";' : "";
701
+ const kvBind = usesKv ? "\n RATE_CREDIT_KV: jsonFileKvStore(KV_PATH)," : "";
702
+ const mailboxBind = usesEmail ? "\n SULUK_MAILBOX_SINK: mailbox," : "";
703
+ // mock-until-keyed: only inject the Stripe fake when there is no real key (a provisioned app hits real Stripe).
704
+ const stripeInject = usesBilling
705
+ ? '\nif (!env.STRIPE_SECRET_KEY) { env.STRIPE_SECRET_KEY = "sk_mock_local"; env.STRIPE_FETCH = mockStripeFetch(); }'
706
+ : "";
707
+ const mailboxRoute = usesEmail
708
+ ? '\n// a dev-only inbox view of the emails the mock provider captured (never mounted on the deployed Worker).\napp.get("/api/email/dev/mailbox", async (c) => c.json(await mailbox.list()));\n'
709
+ : "";
710
+ return `// AUTO-GENERATED by @suluk/platform — the bun MOCK-PROVIDER dev server. Runs the wired app under bun with a
711
+ // bun:sqlite DB + JSON-file KV, so \`bun run dev\` works with ZERO Cloudflare account and no wrangler. A provider goes REAL
712
+ // the moment its key is present (mock-until-keyed): add real keys to .env.temp + \`bun run provision\` and this file uses
713
+ // them. NOTE: src/index.ts (the deployed Worker) imports NONE of these mocks — bun:sqlite never enters the Worker bundle.
714
+ import { app } from "./index";
715
+ import { Database } from "bun:sqlite";
716
+ import { ${localImports.join(", ")} } from "@suluk/cloudflare/local";${billingImport}
717
+ import { loadEnvFile } from "@suluk/env/node";
718
+
719
+ const DB_PATH = process.env.SULUK_DB_PATH ?? ".suluk/dev.sqlite";${usesKv ? '\nconst KV_PATH = process.env.SULUK_KV_PATH ?? ".suluk/dev-kv.json";' : ""}${usesEmail ? '\nconst MAILBOX_PATH = process.env.SULUK_MAILBOX_PATH ?? ".suluk/dev-mailbox.json";' : ""}
720
+ const PORT = Number(process.env.PORT ?? 8787);
721
+
722
+ const sqlite = new Database(DB_PATH, { create: true });
723
+ const tables = await applyLocalSchema(sqlite); // discover src/db/*.ts + create the tables from the drizzle schema
724
+ console.log(\`[suluk dev] sqlite \${DB_PATH} — \${tables.length} tables\`);
725
+ ${usesEmail ? "const mailbox = jsonFileMailbox(MAILBOX_PATH); // a local inbox the mock email provider saves to\n" : ""}
726
+ // Real secrets (if this app has been provisioned): decrypt the committed .env with the local private key. Fresh app / no
727
+ // key → {} → every provider mocks. Best-effort: a decryption failure never blocks the mock path.
728
+ let secrets: Record<string, string> = {};
729
+ try { secrets = await loadEnvFile(); } catch {}
730
+
731
+ // The request env: process.env < decrypted secrets < the mock bindings. DB/KV are always local (a bun process can't bind a
732
+ // remote D1/KV); the HTTP providers (Google/Stripe/Resend) use their real key when present, else their module's mock.
733
+ const env: Record<string, unknown> = {
734
+ ...process.env,
735
+ ...secrets,
736
+ DB: d1FromSqlite(sqlite),${kvBind}${mailboxBind}
737
+ };
738
+
739
+ const mocked = ["GOOGLE_CLIENT_ID", "STRIPE_SECRET_KEY", "RESEND_API_KEY"].filter((k) => !env[k]);
740
+ if (mocked.length) console.log(\`[suluk dev] mocked (no key): \${mocked.join(", ")}\`);${stripeInject}
741
+ ${mailboxRoute}
742
+ const ctx = { waitUntil() {}, passThroughOnException() {} } as unknown as ExecutionContext;
743
+ Bun.serve({ port: PORT, idleTimeout: 120, fetch: (req) => app.fetch(req, env as Parameters<typeof app.fetch>[1], ctx) });
744
+ console.log(\`[suluk dev] → http://localhost:\${PORT} (mock-until-keyed; provision to go live)\`);
745
+ `;
746
+ }
747
+
677
748
  function buildProvisionConfig(services: string[], catalog: Record<string, Service> = CORE_SERVICES): string {
678
749
  const frags = services.map((s) => catalog[s].provision).filter((p): p is NonNullable<typeof p> => !!p);
679
750
  const imports = frags.map((f) => `import { ${f.symbol} } from "${f.from}";`);
package/src/resolve.ts CHANGED
@@ -87,6 +87,7 @@ export function liftLegacy(m: PlatformManifest): Platform {
87
87
  services: m.services,
88
88
  ...(Object.keys(globalServiceOpts).length ? { globalServiceOpts } : {}),
89
89
  ...(m.opts && Object.keys(m.opts).length ? { serviceOpts: m.opts } : {}),
90
+ ...(m.local ? { local: true } : {}),
90
91
  },
91
92
  brand: {
92
93
  name: m.name,
@@ -107,5 +108,6 @@ export function liftSystemBrand(p: Platform): PlatformManifest {
107
108
  services,
108
109
  ...(Object.keys(opts).length ? { opts } : {}),
109
110
  ...(Object.keys(vars).length ? { vars } : {}),
111
+ ...(p.system.local ? { local: true } : {}),
110
112
  };
111
113
  }