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.
- package/package.json +1 -1
- package/templates/minimal-app/.cursor/rules/frontend-data-guide.mdc +1 -0
- package/templates/minimal-app/.cursor/rules/server-event-emit.mdc +41 -0
- package/templates/minimal-app/AGENTS.md.tpl +1 -0
- package/templates/minimal-app/CLAUDE.md.tpl +1 -0
- package/templates/minimal-app/README.md.tpl +2 -1
- package/templates/minimal-app/apps/server/AGENTS.md.tpl +1 -0
- package/templates/minimal-app/apps/server/src/modules/demo-workflow/demo-workflow.module.ts.tpl +7 -3
- package/templates/minimal-app/apps/server/src/modules/demo-workflow/demo-workflow.service.ts.tpl +12 -4
- package/templates/minimal-app/apps/server/src/modules/posts/posts.module.ts.tpl +7 -4
- package/templates/minimal-app/apps/server/src/modules/posts/posts.service.ts.tpl +1 -1
- package/templates/minimal-app/apps/shared/package.json.tpl +1 -0
- package/templates/minimal-app/apps/shared/src/modules/demo-workflow/demo-workflow.constants.ts.tpl +1 -0
- package/templates/minimal-app/apps/webapp/src/App.tsx.tpl +8 -5
- package/templates/minimal-app/apps/webapp/src/StarterServerEventProvider.tsx.tpl +38 -0
- package/templates/minimal-app/package.json.tpl +5 -4
- package/templates/minimal-app/pnpm-workspace.yaml.tpl +11 -11
- package/templates/minimal-app/template.manifest.json +1 -0
- package/templates/minimal-app/apps/landing/scripts/fly-deploy.mjs.tpl +0 -75
- package/templates/minimal-app/apps/landing/scripts/fly-secrets.mjs.tpl +0 -31
- package/templates/minimal-app/apps/shared/scripts/fly-deploy.mjs.tpl +0 -75
- package/templates/minimal-app/apps/shared/scripts/fly-secrets.mjs.tpl +0 -31
package/package.json
CHANGED
|
@@ -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
|
|
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.
|
package/templates/minimal-app/apps/server/src/modules/demo-workflow/demo-workflow.module.ts.tpl
CHANGED
|
@@ -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({
|
|
35
|
+
demoWorkflow: new DemoWorkflowService({
|
|
36
|
+
workflow: deps.workflow.services.workflow,
|
|
37
|
+
auth: deps.auth.services.auth,
|
|
38
|
+
}),
|
|
35
39
|
};
|
|
36
40
|
}
|
|
37
41
|
|
package/templates/minimal-app/apps/server/src/modules/demo-workflow/demo-workflow.service.ts.tpl
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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<
|
|
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({
|
|
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<
|
|
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",
|
package/templates/minimal-app/apps/shared/src/modules/demo-workflow/demo-workflow.constants.ts.tpl
ADDED
|
@@ -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
|
-
<
|
|
35
|
-
<
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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": "
|
|
12
|
-
"app:secrets": "
|
|
13
|
-
"landing:deploy": "
|
|
14
|
-
"landing:secrets": "
|
|
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.
|
|
11
|
-
"@m5kdev/commons": 0.
|
|
12
|
-
"@m5kdev/config": 0.
|
|
13
|
-
"@m5kdev/email": 0.
|
|
14
|
-
"@m5kdev/frontend": 0.
|
|
15
|
-
"@m5kdev/module-clay": 0.
|
|
16
|
-
"@m5kdev/module-docx": 0.
|
|
17
|
-
"@m5kdev/module-pdf": 0.
|
|
18
|
-
"@m5kdev/module-social": 0.
|
|
19
|
-
"@m5kdev/module-video": 0.
|
|
20
|
-
"@m5kdev/web-ui": 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
|
|
@@ -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);
|