@suluk/platform 0.1.9 → 0.1.10

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,6 +1,6 @@
1
1
  {
2
2
  "name": "@suluk/platform",
3
- "version": "0.1.9",
3
+ "version": "0.1.10",
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
6
  "access": "public"
package/src/generate.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  * testable. Stops short of `provision apply` — that's a live infra op the operator triggers.
6
6
  */
7
7
  import type { PlatformManifest } from "./manifest";
8
- import { planPlatform, mergePackageJson, mergeWranglerToml, type PlatformPlan } from "./plan";
8
+ import { planPlatform, mergePackageJson, mergeWranglerToml, mergeGitignore, type PlatformPlan } from "./plan";
9
9
 
10
10
  export interface GenerateOptions {
11
11
  /** run a command — the CLI spawns `bunx shadcn add <ref>`; a test records. */
@@ -43,10 +43,14 @@ export async function generatePlatform(manifest: PlatformManifest, opts: Generat
43
43
  log("▸ writing wrangler.toml");
44
44
  await opts.write("wrangler.toml", mergeWranglerToml(plan.wranglerToml, await read("wrangler.toml")));
45
45
  written.push("wrangler.toml");
46
+ // .gitignore MERGES (append missing entries) — critically ensures .env/.env.temp are ignored even if the app already had
47
+ // a minimal .gitignore, so secrets are never committed. .env.example + the env-check are always (re)written (no values).
48
+ log("▸ writing .gitignore");
49
+ await opts.write(".gitignore", mergeGitignore(plan.gitignore, await read(".gitignore")));
50
+ written.push(".gitignore");
46
51
  for (const [file, content, always] of [
47
52
  ["tsconfig.json", plan.tsconfig, false],
48
53
  ["components.json", plan.componentsJson, false],
49
- [".gitignore", plan.gitignore, false],
50
54
  [".env.example", plan.envExample, true], // a checked-in template (no values) — keep it current
51
55
  ["scripts/env-check.ts", plan.envCheck, true], // the .env.temp lifecycle preflight
52
56
  ] as const) {
package/src/index.ts CHANGED
@@ -7,5 +7,5 @@
7
7
  export { definePlatform, type PlatformManifest } from "./manifest";
8
8
  export { CATALOG, orderServices, collectEnv, resolveVersion, BASE_DEPS, ECOSYSTEM_VERSIONS, DEV_DEPS, type CatalogEntry, type Mount, type EnvVar } from "./catalog";
9
9
  export { mergeProvision } from "./merge";
10
- export { planPlatform, buildPackageJson, mergePackageJson, mergeWranglerToml, type PlatformPlan } from "./plan";
10
+ export { planPlatform, buildPackageJson, mergePackageJson, mergeWranglerToml, mergeGitignore, type PlatformPlan } from "./plan";
11
11
  export { generatePlatform, type GenerateOptions, type GenerateResult } from "./generate";
package/src/plan.ts CHANGED
@@ -137,6 +137,18 @@ function buildGitignore(): string {
137
137
  return ["node_modules/", ".env", ".env.temp", ".dev.vars", ".wrangler/", "dist/", ""].join("\n");
138
138
  }
139
139
 
140
+ /** Merge the generated .gitignore into an existing one — APPEND any missing entries (never skip-if-present, so an app's
141
+ * minimal .gitignore can't leave `.env`/`.env.temp` UNIGNORED and risk committing secrets). Dedup, preserve app entries. */
142
+ export function mergeGitignore(generated: string, existing: string | null): string {
143
+ if (!existing) return generated;
144
+ const norm = (s: string) => s.trim().replace(/\/$/, "");
145
+ const have = new Set(existing.split("\n").map(norm).filter(Boolean));
146
+ const add = generated.split("\n").filter((l) => l.trim() && !have.has(norm(l)));
147
+ if (!add.length) return existing.endsWith("\n") ? existing : existing + "\n";
148
+ const base = existing.replace(/\n*$/, "");
149
+ return `${base}\n${add.join("\n")}\n`;
150
+ }
151
+
140
152
  /** The `.env.temp` lifecycle preflight (run via `predev` / `bun run check`): if every REQUIRED secret is present (in `.env`
141
153
  * or the process env), delete `.env.temp`; else write `.env.temp` from `.env.example` + report the missing keys + fail. */
142
154
  function buildEnvCheckScript(env: EnvVar[]): string {
package/test/plan.test.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { test, expect, describe } from "bun:test";
2
- import { definePlatform, planPlatform, mergeProvision, generatePlatform, buildPackageJson, mergePackageJson, mergeWranglerToml } from "../src/index";
2
+ import { definePlatform, planPlatform, mergeProvision, generatePlatform, buildPackageJson, mergePackageJson, mergeWranglerToml, mergeGitignore } from "../src/index";
3
3
  import type { InstanceSpec } from "@suluk/provision";
4
4
 
5
5
  /** C051 — the platform generator: manifest → plan (adds + wired entry + merged provision), the provision merge, and the
@@ -162,11 +162,11 @@ describe("generatePlatform — the orchestration (with recorders)", () => {
162
162
  expect(ran).toEqual(plannedAdds()); // exactly the planned adds, in order
163
163
  expect(ran.length).toBe(6); // app+auth+credits+keys+billing+logs
164
164
  // config is written BEFORE the shadcn adds; the glue after. env-example + env-check + wrangler + gitignore included.
165
- expect(wrote).toEqual(["package.json", "wrangler.toml", "tsconfig.json", "components.json", ".gitignore", ".env.example", "scripts/env-check.ts", "src/index.ts", "provision.config.ts"]);
165
+ expect(wrote).toEqual(["package.json", "wrangler.toml", ".gitignore", "tsconfig.json", "components.json", ".env.example", "scripts/env-check.ts", "src/index.ts", "provision.config.ts"]);
166
166
  expect(res.added.length).toBe(6);
167
167
  });
168
168
 
169
- test("leaves an existing tsconfig/components.json/.gitignore untouched; always rewrites package.json/.env.example", async () => {
169
+ test("leaves an existing tsconfig/components.json untouched; always (re)writes package.json/.gitignore/.env.example", async () => {
170
170
  const wrote: string[] = [];
171
171
  await generatePlatform(manifest, {
172
172
  run: async () => {},
@@ -174,10 +174,17 @@ describe("generatePlatform — the orchestration (with recorders)", () => {
174
174
  read: async (p) => (p === "package.json" ? '{"name":"x","dependencies":{"my-lib":"^1.0.0"}}' : "existing"),
175
175
  });
176
176
  expect(wrote).toContain("package.json"); // merged + rewritten
177
+ expect(wrote).toContain(".gitignore"); // MERGED (never skip — must ensure .env is ignored)
177
178
  expect(wrote).toContain(".env.example"); // template — always current
178
179
  expect(wrote).toContain("scripts/env-check.ts");
179
180
  expect(wrote).not.toContain("tsconfig.json"); // present → left as-is
180
- expect(wrote).not.toContain(".gitignore");
181
+ });
182
+
183
+ test("mergeGitignore appends missing entries so .env is always ignored", () => {
184
+ const merged = mergeGitignore("node_modules/\n.env\n.env.temp\n", "node_modules\n");
185
+ expect(merged).toContain(".env");
186
+ expect(merged).toContain(".env.temp");
187
+ expect(merged.match(/node_modules/g)?.length).toBe(1); // deduped (node_modules vs node_modules/)
181
188
  });
182
189
  });
183
190