create-m5kdev 0.34.0 → 0.35.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.
Files changed (22) hide show
  1. package/package.json +1 -1
  2. package/templates/minimal-app/.cursor/rules/frontend-data-guide.mdc +1 -0
  3. package/templates/minimal-app/.cursor/rules/server-event-emit.mdc +41 -0
  4. package/templates/minimal-app/AGENTS.md.tpl +1 -0
  5. package/templates/minimal-app/CLAUDE.md.tpl +1 -0
  6. package/templates/minimal-app/README.md.tpl +2 -1
  7. package/templates/minimal-app/apps/server/AGENTS.md.tpl +1 -0
  8. package/templates/minimal-app/apps/server/src/modules/demo-workflow/demo-workflow.module.ts.tpl +7 -3
  9. package/templates/minimal-app/apps/server/src/modules/demo-workflow/demo-workflow.service.ts.tpl +12 -4
  10. package/templates/minimal-app/apps/server/src/modules/posts/posts.module.ts.tpl +7 -4
  11. package/templates/minimal-app/apps/server/src/modules/posts/posts.service.ts.tpl +1 -1
  12. package/templates/minimal-app/apps/shared/package.json.tpl +1 -0
  13. package/templates/minimal-app/apps/shared/src/modules/demo-workflow/demo-workflow.constants.ts.tpl +1 -0
  14. package/templates/minimal-app/apps/webapp/src/App.tsx.tpl +8 -5
  15. package/templates/minimal-app/apps/webapp/src/StarterServerEventProvider.tsx.tpl +38 -0
  16. package/templates/minimal-app/package.json.tpl +5 -4
  17. package/templates/minimal-app/pnpm-workspace.yaml.tpl +11 -11
  18. package/templates/minimal-app/template.manifest.json +1 -0
  19. package/templates/minimal-app/apps/landing/scripts/fly-deploy.mjs.tpl +0 -75
  20. package/templates/minimal-app/apps/landing/scripts/fly-secrets.mjs.tpl +0 -31
  21. package/templates/minimal-app/apps/shared/scripts/fly-deploy.mjs.tpl +0 -75
  22. package/templates/minimal-app/apps/shared/scripts/fly-secrets.mjs.tpl +0 -31
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-m5kdev",
3
- "version": "0.34.0",
3
+ "version": "0.35.0",
4
4
  "license": "GPL-3.0-only",
5
5
  "engines": {
6
6
  "node": ">=24"
@@ -88,6 +88,7 @@ Mutation input shapes are enforced by the router's inferred types — no
88
88
 
89
89
  - Invalidate intentionally with the matching `queryFilter()` after mutations.
90
90
  - Never call `queryClient.clear()` or invalidate everything.
91
+ - Background Workflow completions: invalidate from a `ServerEventProvider` handler. The trigger mutation only returns `jobId`. See `.cursor/rules/server-event-emit.mdc`.
91
92
 
92
93
  ## Pending state
93
94
 
@@ -0,0 +1,41 @@
1
+ ---
2
+ description: Emit Server events from background jobs, not from mutations that already return the entity
3
+ globs: **/*.service.ts,**/*.jobs.ts,**/App.tsx,**/*ServerEventProvider.tsx
4
+ alwaysApply: false
5
+ ---
6
+
7
+ # Server event emit
8
+
9
+ Emit through Auth (`userEmit`, `batchUserEmit`, `organizationEmit`, `emitServerEvent`) when work finishes **off the request** that started it. Typical case: a Workflow job. The UI will not get a completion payload, so SSE tells it to refetch or patch cache.
10
+
11
+ The acting User already receives the mutation result. They do not need an SSE for that write.
12
+
13
+ ## Emit from
14
+
15
+ - Workflow job `.handle` after the job creates or updates a resource
16
+ - Other background work the User did not wait on (cron, inbound callback that persisted something)
17
+
18
+ ```typescript
19
+ .handle(async (payload) => {
20
+ // ... job work ...
21
+ this.service.auth.userEmit({
22
+ userId: payload.userId,
23
+ resource: "workflow",
24
+ id: jobName,
25
+ change: "updated",
26
+ organizationId: null,
27
+ });
28
+ });
29
+ ```
30
+
31
+ Call Auth directly. Do not wrap `userEmit` in a private helper that only forwards.
32
+
33
+ ## Organization fan-out
34
+
35
+ `organizationEmit` (or `emitServerEvent` with an Organization id) notifies **other Members**. Use it when a background job (or similar off-request work) creates or changes an org-scoped entity those Members must see. Skip it for tRPC writes that already return the entity, including publish.
36
+
37
+ ## Client
38
+
39
+ Mount `ServerEventProvider` in the app shell. Register a handler that invalidates the matching `queryFilter()`. Reconnect has no replay: `onReconnect` should invalidate the same queries. Keep polling only as a fallback for in-progress status (queued → running) if you emit only when the job finishes.
40
+
41
+ Starter: `DemoWorkflowService` `demo.ping` + `trpc.workflow.list`.
@@ -25,6 +25,7 @@
25
25
  - Do not add service methods that merely call another service. Call the owning service directly unless the method adds a business rule, authorization, validation, orchestration, a transaction boundary, or meaningful domain translation.
26
26
  - Renaming a dependency method, repackaging arguments, constructing a prompt, or forwarding actor/context data alone does not justify a service wrapper.
27
27
  - Do not create Drizzle migrations by hand. Use the scaffolded config and your project migration workflow later if you need generated migrations.
28
+ - Server event: emit through Auth from Workflow job handlers (and similar background work) when the UI must refresh a change the triggering mutation did not return. See `.cursor/rules/server-event-emit.mdc`.
28
29
 
29
30
  ## Frontend Conventions
30
31
 
@@ -117,6 +117,7 @@ The framework's detailed conventions live in `.cursor/rules/*.mdc`. Cursor appli
117
117
  | `.cursor/rules/module-dto-guide.mdc` | `apps/server/src/modules/**/*.dto.ts` |
118
118
  | `.cursor/rules/module-repository-guide.mdc` | `apps/server/src/modules/**/*.repository.ts` |
119
119
  | `.cursor/rules/module-service-guide.mdc` | `apps/server/src/modules/**/*.service.ts` |
120
+ | `.cursor/rules/server-event-emit.mdc` | `*.service.ts`, `*.jobs.ts`, webapp `App.tsx` / `*ServerEventProvider.tsx` |
120
121
  | `.cursor/rules/module-trpc-guide.mdc` | `apps/server/src/modules/**/*.trpc.ts` |
121
122
  | `.cursor/rules/module-grants-guide.mdc` | `apps/server/src/modules/**/*.grants.ts` |
122
123
  | `.cursor/rules/module-module-guide.mdc` | `apps/server/src/modules/**/*.module.ts` |
@@ -76,7 +76,8 @@ pnpm landing:deploy
76
76
  ```
77
77
 
78
78
  `app:deploy` / `landing:deploy` read that app’s `.env.production` and forward
79
- every key as `fly deploy --build-secret`. A missing env file errors with a
79
+ every key as `fly deploy --build-secret` via Kernel bins
80
+ (`m5kdev-fly-deploy` / `m5kdev-fly-secrets`). A missing env file errors with a
80
81
  copy-the-example message. Product data lives on the `libsql_data` volume at
81
82
  `/app/data` (`DATABASE_URL=file:/app/data/local.db`). Redis is a runtime secret,
82
83
  not an image layer.
@@ -45,6 +45,7 @@ apps/server/src/modules/<module>/
45
45
 
46
46
  // m5k:workflows:start
47
47
  - `WorkflowModule` is registered in `app.ts`. Start **Redis** locally (`REDIS_URL`) before `pnpm dev` on the server, or background jobs will not run.
48
+ - Server event: emit through Auth from a job `.handle` when the UI must refresh a change the trigger mutation did not return. See `.cursor/rules/server-event-emit.mdc`.
48
49
  // m5k:workflows:end
49
50
  - `index.ts` calls `builtBackendApp.start()`, which listens and handles SIGINT/SIGTERM. Extra shutdown work (telemetry) is `onShutdown` on `createBackendApp`.
50
51
  - After changing Drizzle tables, run `pnpm --filter ./apps/server drizzle:generate` then `drizzle:migrate` — do not hand-edit SQL migrations in this repo.
@@ -1,4 +1,5 @@
1
1
  import { createBackendRouterMap } from "@m5kdev/backend/app";
2
+ import type { AuthModule } from "@m5kdev/backend/modules/auth/auth.module";
2
3
  import {
3
4
  BaseModule,
4
5
  type ModuleServicesContext,
@@ -9,7 +10,7 @@ import type { WorkflowModule } from "@m5kdev/backend/modules/workflow/workflow.m
9
10
  import { DemoWorkflowService } from "./demo-workflow.service";
10
11
  import { createDemoWorkflowTRPC } from "./demo-workflow.trpc";
11
12
 
12
- type DemoWorkflowModuleDeps = { workflow: WorkflowModule };
13
+ type DemoWorkflowModuleDeps = { workflow: WorkflowModule; auth: AuthModule };
13
14
  type DemoWorkflowModuleServices = {
14
15
  demoWorkflow: DemoWorkflowService;
15
16
  };
@@ -25,13 +26,16 @@ export class DemoWorkflowModule extends BaseModule<
25
26
  DemoWorkflowModuleRouters
26
27
  > {
27
28
  readonly id = "demo-workflow";
28
- override readonly dependsOn = ["workflow"] as const;
29
+ override readonly dependsOn = ["workflow", "auth"] as const;
29
30
 
30
31
  override services({
31
32
  deps,
32
33
  }: ModuleServicesContext<DemoWorkflowModuleDeps, Record<string, never>>) {
33
34
  return {
34
- demoWorkflow: new DemoWorkflowService({ workflow: deps.workflow.services.workflow }),
35
+ demoWorkflow: new DemoWorkflowService({
36
+ workflow: deps.workflow.services.workflow,
37
+ auth: deps.auth.services.auth,
38
+ }),
35
39
  };
36
40
  }
37
41
 
@@ -1,9 +1,11 @@
1
+ import type { AuthService } from "@m5kdev/backend/modules/auth/auth.service";
1
2
  import type { ServerResultAsync } from "@m5kdev/backend/modules/base/base.dto";
2
3
  import { BaseService } from "@m5kdev/backend/modules/base/base.service";
3
4
  import type { WorkflowService } from "@m5kdev/backend/modules/workflow/workflow.service";
4
5
  import type { FireAndForgetJobDefinition } from "@m5kdev/backend/modules/workflow/workflow.types";
5
6
  import type { Context } from "@m5kdev/backend/utils/trpc";
6
7
  import { workflowTriggerOutputSchema } from "@m5kdev/commons/modules/workflow/workflow.schema";
8
+ import { DEMO_WORKFLOW_SERVER_EVENT_RESOURCE } from "{{PACKAGE_SCOPE}}/shared/modules/demo-workflow/demo-workflow.constants";
7
9
  import { ok } from "neverthrow";
8
10
 
9
11
  const DEMO_PING_JOB_NAME = "demo.ping";
@@ -14,12 +16,12 @@ interface DemoPingJobPayload {
14
16
 
15
17
  export class DemoWorkflowService extends BaseService<
16
18
  Record<string, never>,
17
- { workflow: WorkflowService },
19
+ { workflow: WorkflowService; auth: AuthService },
18
20
  Context
19
21
  > {
20
22
  readonly demoPingJob: FireAndForgetJobDefinition<DemoPingJobPayload>;
21
23
 
22
- constructor(services: { workflow: WorkflowService }) {
24
+ constructor(services: { workflow: WorkflowService; auth: AuthService }) {
23
25
  super({}, services);
24
26
 
25
27
  this.demoPingJob = this.service.workflow
@@ -28,8 +30,14 @@ export class DemoWorkflowService extends BaseService<
28
30
  queue: "fast",
29
31
  meta: (payload) => ({ userId: payload.userId }),
30
32
  })
31
- .handle(async () => {
32
- // Completes immediately so the Starter webapp can observe a finished run.
33
+ .handle(async (payload) => {
34
+ this.service.auth.userEmit({
35
+ userId: payload.userId,
36
+ resource: DEMO_WORKFLOW_SERVER_EVENT_RESOURCE,
37
+ id: DEMO_PING_JOB_NAME,
38
+ change: "updated",
39
+ organizationId: null,
40
+ });
33
41
  });
34
42
  }
35
43
 
@@ -11,6 +11,7 @@ import { PostsRepository } from "./posts.repository";
11
11
  import { PostsService } from "./posts.service";
12
12
  import { createPostsTRPC } from "./posts.trpc";
13
13
 
14
+ type PostsModuleDeps = Record<string, never>;
14
15
  type PostsModuleTables = typeof postsTables;
15
16
  type PostsModuleRepositories = {
16
17
  posts: PostsRepository;
@@ -23,7 +24,7 @@ type PostsModuleRouters = {
23
24
  };
24
25
 
25
26
  export class PostsModule extends BaseModule<
26
- never,
27
+ PostsModuleDeps,
27
28
  PostsModuleTables,
28
29
  PostsModuleRepositories,
29
30
  PostsModuleServices,
@@ -32,7 +33,7 @@ export class PostsModule extends BaseModule<
32
33
  readonly id = "posts";
33
34
  override readonly dbDependsOn = ["auth"] as const;
34
35
 
35
- override repositories({ db }: ModuleRepositoriesContext<never, PostsModuleTables>) {
36
+ override repositories({ db }: ModuleRepositoriesContext<PostsModuleDeps, PostsModuleTables>) {
36
37
  return {
37
38
  posts: new PostsRepository({
38
39
  orm: db.orm,
@@ -42,13 +43,15 @@ export class PostsModule extends BaseModule<
42
43
  };
43
44
  }
44
45
 
45
- override services({ repositories }: ModuleServicesContext<never, PostsModuleRepositories>) {
46
+ override services({
47
+ repositories,
48
+ }: ModuleServicesContext<PostsModuleDeps, PostsModuleRepositories>) {
46
49
  return {
47
50
  posts: new PostsService({ posts: repositories.posts }, {}, postsGrants),
48
51
  };
49
52
  }
50
53
 
51
- override trpc({ trpc, services }: ModuleTRPCContext<never, PostsModuleServices>) {
54
+ override trpc({ trpc, services }: ModuleTRPCContext<PostsModuleDeps, PostsModuleServices>) {
52
55
  return createBackendRouterMap("posts", createPostsTRPC(trpc, services.posts));
53
56
  }
54
57
  }
@@ -88,7 +88,7 @@ export class PostsService extends BasePermissionService<
88
88
  action: "publish",
89
89
  entityStep: "post",
90
90
  })
91
- .handle(({ input, state }) => {
91
+ .handle(async ({ input, state }) => {
92
92
  return this.repository.posts.update({
93
93
  id: input.id,
94
94
  status: "published",
@@ -20,6 +20,7 @@
20
20
  "exports": {
21
21
  "./modules/app/*": "./src/modules/app/*.ts",
22
22
  "./modules/conversation/*": "./src/modules/conversation/*.ts",
23
+ "./modules/demo-workflow/*": "./src/modules/demo-workflow/*.ts",
23
24
  "./modules/posts/*": "./src/modules/posts/*.ts"
24
25
  }
25
26
  }
@@ -0,0 +1 @@
1
+ export const DEMO_WORKFLOW_SERVER_EVENT_RESOURCE = "workflow";
@@ -14,6 +14,7 @@ import { NuqsAdapter } from "nuqs/adapters/react-router/v7";
14
14
  import { BrowserRouter } from "react-router";
15
15
  import { Toaster } from "sonner";
16
16
  import { Router } from "./Router";
17
+ import { StarterServerEventProvider } from "./StarterServerEventProvider";
17
18
 
18
19
  export function App() {
19
20
  return (
@@ -31,11 +32,13 @@ export function App() {
31
32
  <ThemeProvider defaultTheme="light" storageKey="m5kdev-theme">
32
33
  <AuthProvider loader={<AppLoader />}>
33
34
  <AppTrpcQueryProvider>
34
- <DialogProvider>
35
- <Router />
36
- </DialogProvider>
37
- <Toaster richColors closeButton />
38
- <Toast.Provider placement="bottom end" />
35
+ <StarterServerEventProvider>
36
+ <DialogProvider>
37
+ <Router />
38
+ </DialogProvider>
39
+ <Toaster richColors closeButton />
40
+ <Toast.Provider placement="bottom end" />
41
+ </StarterServerEventProvider>
39
42
  </AppTrpcQueryProvider>
40
43
  </AuthProvider>
41
44
  </ThemeProvider>
@@ -0,0 +1,38 @@
1
+ // biome-ignore-all assist/source/organizeImports: feature-gated Server event imports stay in marker blocks
2
+ import { ServerEventProvider } from "@m5kdev/frontend/modules/app/components/ServerEventProvider";
3
+ // m5k:workflows:start
4
+ import type { QueryClient } from "@tanstack/react-query";
5
+ import { DEMO_WORKFLOW_SERVER_EVENT_RESOURCE } from "{{PACKAGE_SCOPE}}/shared/modules/demo-workflow/demo-workflow.constants";
6
+ // m5k:workflows:end
7
+ import type { ReactNode } from "react";
8
+ // m5k:workflows:start
9
+ import { useTRPC } from "./utils/trpc";
10
+ // m5k:workflows:end
11
+
12
+ type Props = {
13
+ children: ReactNode;
14
+ };
15
+
16
+ /** Kernel SSE for the authenticated session. Workflow handlers are feature-gated. */
17
+ export function StarterServerEventProvider({ children }: Props): ReactNode {
18
+ // m5k:workflows:start
19
+ const trpc = useTRPC();
20
+ const invalidateWorkflowList = (queryClient: QueryClient): void => {
21
+ void queryClient.invalidateQueries(trpc.workflow.list.queryFilter());
22
+ };
23
+ // m5k:workflows:end
24
+ return (
25
+ <ServerEventProvider
26
+ // m5k:workflows:start
27
+ handlers={{
28
+ [DEMO_WORKFLOW_SERVER_EVENT_RESOURCE]: (_event, queryClient) => {
29
+ invalidateWorkflowList(queryClient);
30
+ },
31
+ }}
32
+ onReconnect={invalidateWorkflowList}
33
+ // m5k:workflows:end
34
+ >
35
+ {children}
36
+ </ServerEventProvider>
37
+ );
38
+ }
@@ -8,13 +8,14 @@
8
8
  "lint": "turbo run lint",
9
9
  "lint:fix": "turbo run lint:fix",
10
10
  "check-types": "turbo run check-types",
11
- "app:deploy": "node apps/shared/scripts/fly-deploy.mjs --config apps/shared/fly.toml --dockerfile apps/shared/Dockerfile --env apps/shared/.env.production",
12
- "app:secrets": "node apps/shared/scripts/fly-secrets.mjs --config apps/shared/fly.toml --env apps/shared/.env.production",
13
- "landing:deploy": "node apps/landing/scripts/fly-deploy.mjs --config apps/landing/fly.toml --dockerfile apps/landing/Dockerfile --env apps/landing/.env.production",
14
- "landing:secrets": "node apps/landing/scripts/fly-secrets.mjs --config apps/landing/fly.toml --env apps/landing/.env.production"
11
+ "app:deploy": "m5kdev-fly-deploy --config apps/shared/fly.toml --dockerfile apps/shared/Dockerfile --env apps/shared/.env.production",
12
+ "app:secrets": "m5kdev-fly-secrets --config apps/shared/fly.toml --env apps/shared/.env.production",
13
+ "landing:deploy": "m5kdev-fly-deploy --config apps/landing/fly.toml --dockerfile apps/landing/Dockerfile --env apps/landing/.env.production",
14
+ "landing:secrets": "m5kdev-fly-secrets --config apps/landing/fly.toml --env apps/landing/.env.production"
15
15
  },
16
16
  "devDependencies": {
17
17
  "@biomejs/biome": "catalog:m5kdev",
18
+ "@m5kdev/backend": "catalog:m5kdev",
18
19
  "turbo": "catalog:m5kdev",
19
20
  "typescript": "catalog:m5kdev"
20
21
  },
@@ -7,17 +7,17 @@ catalogs:
7
7
  "@heroui/react": 3.0.3
8
8
  "@heroui/styles": 3.0.3
9
9
  "@libsql/client": 0.17.4
10
- "@m5kdev/backend": 0.34.0
11
- "@m5kdev/commons": 0.34.0
12
- "@m5kdev/config": 0.34.0
13
- "@m5kdev/email": 0.34.0
14
- "@m5kdev/frontend": 0.34.0
15
- "@m5kdev/module-clay": 0.34.0
16
- "@m5kdev/module-docx": 0.34.0
17
- "@m5kdev/module-pdf": 0.34.0
18
- "@m5kdev/module-social": 0.34.0
19
- "@m5kdev/module-video": 0.34.0
20
- "@m5kdev/web-ui": 0.34.0
10
+ "@m5kdev/backend": 0.35.0
11
+ "@m5kdev/commons": 0.35.0
12
+ "@m5kdev/config": 0.35.0
13
+ "@m5kdev/email": 0.35.0
14
+ "@m5kdev/frontend": 0.35.0
15
+ "@m5kdev/module-clay": 0.35.0
16
+ "@m5kdev/module-docx": 0.35.0
17
+ "@m5kdev/module-pdf": 0.35.0
18
+ "@m5kdev/module-social": 0.35.0
19
+ "@m5kdev/module-video": 0.35.0
20
+ "@m5kdev/web-ui": 0.35.0
21
21
  "@mastra/core": 1.51.0
22
22
  "@openrouter/ai-sdk-provider": 3.0.0
23
23
  "@opentelemetry/api": 1.9.0
@@ -40,6 +40,7 @@
40
40
  "paths": [
41
41
  "apps/webapp/src/modules/workflows/",
42
42
  "apps/server/src/modules/demo-workflow/",
43
+ "apps/shared/src/modules/demo-workflow/",
43
44
  "apps/e2e/tests/workflow.spec.ts"
44
45
  ]
45
46
  },
@@ -1,75 +0,0 @@
1
- #!/usr/bin/env node
2
- import { spawnSync } from "node:child_process";
3
- import { existsSync, readFileSync } from "node:fs";
4
- import { parseArgs } from "node:util";
5
-
6
- function missingEnvMessage(envPath) {
7
- return `Missing ${envPath}. Copy the .env.production.example next to it, fill in values, then retry.`;
8
- }
9
-
10
- function parseDotenv(source) {
11
- const vars = {};
12
- for (const rawLine of source.split(/\r?\n/)) {
13
- const line = rawLine.trim();
14
- if (!line || line.startsWith("#")) continue;
15
- const eq = line.indexOf("=");
16
- if (eq === -1) continue;
17
- const key = line.slice(0, eq).trim();
18
- let value = line.slice(eq + 1);
19
- if (
20
- (value.startsWith('"') && value.endsWith('"')) ||
21
- (value.startsWith("'") && value.endsWith("'"))
22
- ) {
23
- value = value.slice(1, -1);
24
- }
25
- vars[key] = value;
26
- }
27
- return vars;
28
- }
29
-
30
- function shellSingleQuote(value) {
31
- return `'${value.replaceAll("'", `'\\''`)}'`;
32
- }
33
-
34
- function toExportScript(vars) {
35
- return Object.entries(vars)
36
- .map(([key, value]) => `export ${key}=${shellSingleQuote(value)}`)
37
- .join("\n");
38
- }
39
-
40
- const { values } = parseArgs({
41
- options: {
42
- config: { type: "string" },
43
- dockerfile: { type: "string" },
44
- env: { type: "string" },
45
- },
46
- });
47
-
48
- if (!values.config || !values.dockerfile || !values.env) {
49
- console.error(
50
- "Usage: fly-deploy.mjs --config <fly.toml> --dockerfile <Dockerfile> --env <.env.production>"
51
- );
52
- process.exit(1);
53
- }
54
-
55
- if (!existsSync(values.env)) {
56
- console.error(missingEnvMessage(values.env));
57
- process.exit(1);
58
- }
59
-
60
- const vars = parseDotenv(readFileSync(values.env, "utf8"));
61
- const args = [
62
- "deploy",
63
- "-c",
64
- values.config,
65
- "--dockerfile",
66
- values.dockerfile,
67
- "--build-secret",
68
- `ALL_SECRETS=${toExportScript(vars)}`,
69
- ];
70
- for (const [key, value] of Object.entries(vars)) {
71
- args.push("--build-secret", `${key}=${value}`);
72
- }
73
-
74
- const result = spawnSync("fly", args, { stdio: "inherit" });
75
- process.exit(result.status ?? 1);
@@ -1,31 +0,0 @@
1
- #!/usr/bin/env node
2
- import { spawnSync } from "node:child_process";
3
- import { existsSync, readFileSync } from "node:fs";
4
- import { parseArgs } from "node:util";
5
-
6
- function missingEnvMessage(envPath) {
7
- return `Missing ${envPath}. Copy the .env.production.example next to it, fill in values, then retry.`;
8
- }
9
-
10
- const { values } = parseArgs({
11
- options: {
12
- config: { type: "string" },
13
- env: { type: "string" },
14
- },
15
- });
16
-
17
- if (!values.config || !values.env) {
18
- console.error("Usage: fly-secrets.mjs --config <fly.toml> --env <.env.production>");
19
- process.exit(1);
20
- }
21
-
22
- if (!existsSync(values.env)) {
23
- console.error(missingEnvMessage(values.env));
24
- process.exit(1);
25
- }
26
-
27
- const result = spawnSync("fly", ["secrets", "import", "-c", values.config], {
28
- input: readFileSync(values.env),
29
- stdio: ["pipe", "inherit", "inherit"],
30
- });
31
- process.exit(result.status ?? 1);
@@ -1,75 +0,0 @@
1
- #!/usr/bin/env node
2
- import { spawnSync } from "node:child_process";
3
- import { existsSync, readFileSync } from "node:fs";
4
- import { parseArgs } from "node:util";
5
-
6
- function missingEnvMessage(envPath) {
7
- return `Missing ${envPath}. Copy the .env.production.example next to it, fill in values, then retry.`;
8
- }
9
-
10
- function parseDotenv(source) {
11
- const vars = {};
12
- for (const rawLine of source.split(/\r?\n/)) {
13
- const line = rawLine.trim();
14
- if (!line || line.startsWith("#")) continue;
15
- const eq = line.indexOf("=");
16
- if (eq === -1) continue;
17
- const key = line.slice(0, eq).trim();
18
- let value = line.slice(eq + 1);
19
- if (
20
- (value.startsWith('"') && value.endsWith('"')) ||
21
- (value.startsWith("'") && value.endsWith("'"))
22
- ) {
23
- value = value.slice(1, -1);
24
- }
25
- vars[key] = value;
26
- }
27
- return vars;
28
- }
29
-
30
- function shellSingleQuote(value) {
31
- return `'${value.replaceAll("'", `'\\''`)}'`;
32
- }
33
-
34
- function toExportScript(vars) {
35
- return Object.entries(vars)
36
- .map(([key, value]) => `export ${key}=${shellSingleQuote(value)}`)
37
- .join("\n");
38
- }
39
-
40
- const { values } = parseArgs({
41
- options: {
42
- config: { type: "string" },
43
- dockerfile: { type: "string" },
44
- env: { type: "string" },
45
- },
46
- });
47
-
48
- if (!values.config || !values.dockerfile || !values.env) {
49
- console.error(
50
- "Usage: fly-deploy.mjs --config <fly.toml> --dockerfile <Dockerfile> --env <.env.production>"
51
- );
52
- process.exit(1);
53
- }
54
-
55
- if (!existsSync(values.env)) {
56
- console.error(missingEnvMessage(values.env));
57
- process.exit(1);
58
- }
59
-
60
- const vars = parseDotenv(readFileSync(values.env, "utf8"));
61
- const args = [
62
- "deploy",
63
- "-c",
64
- values.config,
65
- "--dockerfile",
66
- values.dockerfile,
67
- "--build-secret",
68
- `ALL_SECRETS=${toExportScript(vars)}`,
69
- ];
70
- for (const [key, value] of Object.entries(vars)) {
71
- args.push("--build-secret", `${key}=${value}`);
72
- }
73
-
74
- const result = spawnSync("fly", args, { stdio: "inherit" });
75
- process.exit(result.status ?? 1);
@@ -1,31 +0,0 @@
1
- #!/usr/bin/env node
2
- import { spawnSync } from "node:child_process";
3
- import { existsSync, readFileSync } from "node:fs";
4
- import { parseArgs } from "node:util";
5
-
6
- function missingEnvMessage(envPath) {
7
- return `Missing ${envPath}. Copy the .env.production.example next to it, fill in values, then retry.`;
8
- }
9
-
10
- const { values } = parseArgs({
11
- options: {
12
- config: { type: "string" },
13
- env: { type: "string" },
14
- },
15
- });
16
-
17
- if (!values.config || !values.env) {
18
- console.error("Usage: fly-secrets.mjs --config <fly.toml> --env <.env.production>");
19
- process.exit(1);
20
- }
21
-
22
- if (!existsSync(values.env)) {
23
- console.error(missingEnvMessage(values.env));
24
- process.exit(1);
25
- }
26
-
27
- const result = spawnSync("fly", ["secrets", "import", "-c", values.config], {
28
- input: readFileSync(values.env),
29
- stdio: ["pipe", "inherit", "inherit"],
30
- });
31
- process.exit(result.status ?? 1);