@tulipes/cli 0.1.0 → 0.2.0-rc.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/README.md +11 -8
- package/dist/agent.d.ts +12 -0
- package/dist/agent.js +29 -0
- package/dist/agent.js.map +1 -0
- package/dist/app-core.d.ts +8 -6
- package/dist/app-core.js +23 -14
- package/dist/app-core.js.map +1 -1
- package/dist/bin.js +3 -3
- package/dist/bin.js.map +1 -1
- package/dist/config-check.d.ts +8 -0
- package/dist/config-check.js +43 -0
- package/dist/config-check.js.map +1 -0
- package/dist/config-import.d.ts +20 -0
- package/dist/config-import.js +133 -0
- package/dist/config-import.js.map +1 -0
- package/dist/dev.d.ts +4 -4
- package/dist/dev.js +7 -10
- package/dist/dev.js.map +1 -1
- package/dist/init.d.ts +6 -11
- package/dist/init.js +17 -6420
- package/dist/init.js.map +1 -1
- package/dist/inspection.d.ts +9 -8
- package/dist/inspection.js +10 -11
- package/dist/inspection.js.map +1 -1
- package/dist/main.js +82 -11
- package/dist/main.js.map +1 -1
- package/dist/migrate.d.ts +30 -0
- package/dist/migrate.js +772 -0
- package/dist/migrate.js.map +1 -0
- package/dist/new-module.js +87 -46
- package/dist/new-module.js.map +1 -1
- package/dist/routes.d.ts +3 -5
- package/dist/routes.js +4 -6
- package/dist/routes.js.map +1 -1
- package/dist/spec.d.ts +2 -5
- package/dist/spec.js +13 -6
- package/dist/spec.js.map +1 -1
- package/dist/sync.d.ts +3 -11
- package/dist/sync.js +81 -20
- package/dist/sync.js.map +1 -1
- package/dist/template.d.ts +58 -0
- package/dist/template.js +244 -0
- package/dist/template.js.map +1 -0
- package/dist/update.js +4 -4
- package/package.json +17 -16
- package/templates/AGENTS.md +162 -0
- package/templates/CLAUDE.md +11 -118
- package/templates/app/.envs/.env.development +18 -0
- package/templates/app/.envs/.env.production +19 -0
- package/templates/app/.envs/.env.staging +19 -0
- package/templates/app/README.md +327 -0
- package/templates/app/app.ts +3 -0
- package/templates/app/config/app.config.ts +73 -0
- package/templates/app/docs/agent-manifest.json +724 -0
- package/templates/app/ecosystem.config.cjs +60 -0
- package/templates/app/modules/agent/controllers/agent.controllers.ts +31 -0
- package/templates/app/modules/agent/i18n/en.json +5 -0
- package/templates/app/modules/agent/i18n/fr.json +5 -0
- package/templates/app/modules/agent/module.acl.ts +14 -0
- package/templates/app/modules/agent/package.json +21 -0
- package/templates/app/modules/agent/routes/agent.routes.ts +29 -0
- package/templates/app/modules/agent/schemas/agent.schemas.ts +9 -0
- package/templates/app/modules/auth/bootstrap/credentials.bootstrap.ts +26 -0
- package/templates/app/modules/auth/bootstrap/seed-admin.bootstrap.ts +44 -0
- package/templates/app/modules/auth/controllers/auth.controllers.ts +312 -0
- package/templates/app/modules/auth/helpers/access-check.ts +42 -0
- package/templates/app/modules/auth/helpers/auth.ts +48 -0
- package/templates/app/modules/auth/helpers/documents.ts +57 -0
- package/templates/app/modules/auth/helpers/passport.ts +180 -0
- package/templates/app/modules/auth/helpers/passwords.ts +58 -0
- package/templates/app/modules/auth/helpers/session-cache.ts +67 -0
- package/templates/app/modules/auth/helpers/tokens.ts +329 -0
- package/templates/app/modules/auth/i18n/en.json +19 -0
- package/templates/app/modules/auth/i18n/fr.json +19 -0
- package/templates/app/modules/auth/lib/index.ts +12 -0
- package/templates/app/modules/auth/models/credential.model.ts +31 -0
- package/templates/app/modules/auth/models/revoked-token.model.ts +20 -0
- package/templates/app/modules/auth/models/session.model.ts +42 -0
- package/templates/app/modules/auth/module.acl.ts +41 -0
- package/templates/app/modules/auth/module.config.ts +30 -0
- package/templates/app/modules/auth/package.json +61 -0
- package/templates/app/modules/auth/routes/auth.routes.ts +94 -0
- package/templates/app/modules/auth/schemas/auth.schemas.ts +37 -0
- package/templates/app/modules/auth/tests/auth.test.ts +1079 -0
- package/templates/app/modules/config/controllers/config.controllers.ts +89 -0
- package/templates/app/modules/config/helpers/audit.ts +41 -0
- package/templates/app/modules/config/helpers/describe.ts +72 -0
- package/templates/app/modules/config/i18n/en.json +6 -0
- package/templates/app/modules/config/i18n/fr.json +6 -0
- package/templates/app/modules/config/models/config-audit.model.ts +26 -0
- package/templates/app/modules/config/module.acl.ts +12 -0
- package/templates/app/modules/config/package.json +23 -0
- package/templates/app/modules/config/routes/config.routes.ts +37 -0
- package/templates/app/modules/config/schemas/config.schemas.ts +13 -0
- package/templates/app/modules/config/tests/config.test.ts +126 -0
- package/templates/app/modules/core/bootstrap/cache.bootstrap.ts +12 -0
- package/templates/app/modules/core/helpers/cache/index.ts +60 -0
- package/templates/app/modules/core/helpers/cache/memory-cache.ts +53 -0
- package/templates/app/modules/core/helpers/cache/redis-cache.ts +68 -0
- package/templates/app/modules/core/helpers/cache/types.ts +37 -0
- package/templates/app/modules/core/helpers/lazy.ts +13 -0
- package/templates/app/modules/core/helpers/logger/index.ts +115 -0
- package/templates/app/modules/core/helpers/mongoose/index.ts +27 -0
- package/templates/app/modules/core/helpers/mongoose/plugins/base.plugin.ts +14 -0
- package/templates/app/modules/core/helpers/mongoose/plugins/soft-delete.plugin.ts +124 -0
- package/templates/app/modules/core/helpers/mongoose/plugins/timestamps.plugin.ts +13 -0
- package/templates/app/modules/core/helpers/mongoose/plugins/types.ts +45 -0
- package/templates/app/modules/core/helpers/mongoose/register.ts +20 -0
- package/templates/app/modules/core/lib/index.ts +34 -0
- package/templates/app/modules/core/module.config.ts +24 -0
- package/templates/app/modules/core/package.json +80 -0
- package/templates/app/modules/core/routes/core.routes.ts +24 -0
- package/templates/app/modules/core/tests/core.test.ts +19 -0
- package/templates/app/modules/hello/agent/hello.agent.ts +33 -0
- package/templates/app/modules/hello/bootstrap/seed-greetings.bootstrap.ts +27 -0
- package/templates/app/modules/hello/controllers/greetings.controllers.ts +43 -0
- package/templates/app/modules/hello/controllers/hello.controllers.ts +50 -0
- package/templates/app/modules/hello/helpers/greeting.ts +8 -0
- package/templates/app/modules/hello/i18n/en.json +8 -0
- package/templates/app/modules/hello/i18n/fr.json +8 -0
- package/templates/app/modules/hello/models/greeting.model.ts +24 -0
- package/templates/app/modules/hello/module.acl.ts +16 -0
- package/templates/app/modules/hello/module.config.ts +14 -0
- package/templates/app/modules/hello/package.json +24 -0
- package/templates/app/modules/hello/queues/hello.queues.ts +38 -0
- package/templates/app/modules/hello/routes/greetings.routes.ts +25 -0
- package/templates/app/modules/hello/routes/hello.routes.ts +44 -0
- package/templates/app/modules/hello/sockets/hello.sockets.ts +21 -0
- package/templates/app/modules/hello/tests/hello.test.ts +19 -0
- package/templates/app/modules/security/bootstrap/rate-limit.bootstrap.ts +8 -0
- package/templates/app/modules/security/helpers/hardening.ts +65 -0
- package/templates/app/modules/security/helpers/proxy.ts +16 -0
- package/templates/app/modules/security/helpers/rate-limit.ts +150 -0
- package/templates/app/modules/security/module.config.ts +59 -0
- package/templates/app/modules/security/package.json +26 -0
- package/templates/app/modules/security/routes/security.routes.ts +42 -0
- package/templates/app/modules/security/tests/security.test.ts +174 -0
- package/templates/app/modules/settings/bootstrap/log-streams.bootstrap.ts +12 -0
- package/templates/app/modules/settings/controllers/settings.controllers.ts +58 -0
- package/templates/app/modules/settings/helpers/access-lease.ts +99 -0
- package/templates/app/modules/settings/helpers/log-stream.ts +272 -0
- package/templates/app/modules/settings/helpers/log-tail.ts +253 -0
- package/templates/app/modules/settings/i18n/en.json +3 -0
- package/templates/app/modules/settings/i18n/fr.json +3 -0
- package/templates/app/modules/settings/module.acl.ts +12 -0
- package/templates/app/modules/settings/package.json +21 -0
- package/templates/app/modules/settings/routes/settings.routes.ts +33 -0
- package/templates/app/modules/settings/schemas/settings.schemas.ts +8 -0
- package/templates/app/modules/settings/tests/settings.test.ts +409 -0
- package/templates/app/modules/users/controllers/users.controllers.ts +113 -0
- package/templates/app/modules/users/i18n/en.json +14 -0
- package/templates/app/modules/users/i18n/fr.json +14 -0
- package/templates/app/modules/users/models/user.model.ts +18 -0
- package/templates/app/modules/users/module.acl.ts +10 -0
- package/templates/app/modules/users/module.config.ts +11 -0
- package/templates/app/modules/users/package.json +24 -0
- package/templates/app/modules/users/queues/users.queues.ts +31 -0
- package/templates/app/modules/users/routes/users.routes.ts +60 -0
- package/templates/app/modules/users/schemas/users.schemas.ts +30 -0
- package/templates/app/modules/users/sockets/users.sockets.ts +18 -0
- package/templates/app/nginx/development.conf +44 -0
- package/templates/app/nginx/production.conf +75 -0
- package/templates/app/nginx/staging.conf +51 -0
- package/templates/app/package.json +58 -0
- package/templates/app/scripts/count-greetings.ts +29 -0
- package/templates/app/scripts/deploy-nginx.ts +143 -0
- package/templates/app/scripts/list-users.ts +34 -0
- package/templates/app/tests/app.test.ts +28 -0
- package/templates/app/tests/support.ts +36 -0
- package/templates/app/tsconfig.json +28 -0
- package/templates/app/vitest.config.ts +6 -0
- package/templates/app/worker.ts +6 -0
- package/templates/claude/skills/tulipes-agent-tool/SKILL.md +83 -0
- package/templates/claude/skills/tulipes-boot-errors/SKILL.md +11 -5
- package/templates/claude/skills/tulipes-config/SKILL.md +74 -0
- package/templates/claude/skills/tulipes-endpoint/SKILL.md +38 -25
- package/templates/claude/skills/tulipes-env-variable/SKILL.md +54 -26
- package/templates/claude/skills/tulipes-model/SKILL.md +11 -4
- package/templates/claude/skills/tulipes-module/SKILL.md +11 -10
- package/templates/claude/skills/tulipes-queue/SKILL.md +20 -6
- package/templates/claude/skills/tulipes-settings/SKILL.md +49 -100
- package/templates/claude/skills/tulipes-spec/SKILL.md +10 -12
- package/templates/minimal/README.md +21 -0
- package/templates/minimal/app.ts +3 -0
- package/templates/minimal/config/app.config.ts +16 -0
- package/templates/minimal/modules/agent/module.acl.ts +3 -0
- package/templates/minimal/modules/agent/package.json +14 -0
- package/templates/minimal/modules/agent/routes/agent.routes.ts +20 -0
- package/templates/minimal/modules/core/module.acl.ts +4 -0
- package/templates/minimal/modules/core/package.json +21 -0
- package/templates/minimal/modules/hello/i18n/en.json +3 -0
- package/templates/minimal/modules/hello/module.acl.ts +2 -0
- package/templates/minimal/modules/hello/package.json +11 -0
- package/templates/minimal/modules/hello/routes/health.routes.ts +9 -0
- package/templates/minimal/modules/hello/routes/hello.routes.ts +10 -0
- package/templates/minimal/modules/security/package.json +15 -0
- package/templates/minimal/modules/security/routes/security.routes.ts +8 -0
- package/templates/minimal/package.json +38 -0
- package/templates/minimal/tests/app.test.ts +52 -0
- package/templates/minimal/tsconfig.json +22 -0
- package/templates/minimal/vitest.config.ts +5 -0
- package/templates/overlays/no-queues/ecosystem.config.cjs +52 -0
- package/templates/overlays/no-queues/modules/hello/controllers/greetings.controllers.ts +41 -0
- package/templates/overlays/no-queues/modules/users/controllers/users.controllers.ts +107 -0
- package/templates/versions.json +11 -0
- package/dist/minimal.d.ts +0 -2
- package/dist/minimal.js +0 -2200
- package/dist/minimal.js.map +0 -1
- /package/templates/{browser-auth.md → app/docs/browser-auth.md} +0 -0
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { constants, lstatSync, opendirSync } from "node:fs";
|
|
2
|
+
import { open, type FileHandle } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { HttpError } from "@tulipes/core/http";
|
|
5
|
+
|
|
6
|
+
export const LOG_TAIL_LIMITS = Object.freeze({
|
|
7
|
+
historyBytes: 256 * 1024,
|
|
8
|
+
chunkBytes: 64 * 1024,
|
|
9
|
+
lineBytes: 16 * 1024,
|
|
10
|
+
readBytes: 8 * 1024 * 1024,
|
|
11
|
+
directoryEntries: 1024,
|
|
12
|
+
pollMs: 1000,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export interface TailOptions {
|
|
16
|
+
dir: string;
|
|
17
|
+
/** Exact basename before the optional pino date/index suffix. */
|
|
18
|
+
prefix: string;
|
|
19
|
+
tail: number;
|
|
20
|
+
/** Returning false stops reading immediately. */
|
|
21
|
+
onLine: (line: string) => boolean | void;
|
|
22
|
+
onError?: (code: "LOG_UNAVAILABLE" | "LOG_READ_LIMIT") => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface Tail {
|
|
26
|
+
readonly ready: Promise<void>;
|
|
27
|
+
readonly closed: boolean;
|
|
28
|
+
close(): void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const unsafe = () =>
|
|
32
|
+
new HttpError(409, "logFileUnsafe", {
|
|
33
|
+
code: "LOG_FILE_UNSAFE",
|
|
34
|
+
defaultMessage: "Logs must be regular, unlinked files in a real directory",
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
/** Trusted ancestors are an operator responsibility, as for settings file access. */
|
|
38
|
+
function newestFile(dir: string, prefix: string): string | undefined {
|
|
39
|
+
const parent = lstatSync(dir, { throwIfNoEntry: false });
|
|
40
|
+
if (!parent) return undefined;
|
|
41
|
+
if (!parent.isDirectory() || parent.isSymbolicLink()) throw unsafe();
|
|
42
|
+
const pattern = new RegExp(
|
|
43
|
+
"^" +
|
|
44
|
+
prefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") +
|
|
45
|
+
"(?:\\.\\d{4}-\\d{2}-\\d{2})?(?:\\.\\d+)?\\.log$",
|
|
46
|
+
);
|
|
47
|
+
const handle = opendirSync(dir);
|
|
48
|
+
let newest: { file: string; mtime: number } | undefined;
|
|
49
|
+
let count = 0;
|
|
50
|
+
try {
|
|
51
|
+
for (let entry = handle.readSync(); entry; entry = handle.readSync()) {
|
|
52
|
+
if (++count > LOG_TAIL_LIMITS.directoryEntries) throw unsafe();
|
|
53
|
+
if (!pattern.test(entry.name)) continue;
|
|
54
|
+
const file = join(dir, entry.name);
|
|
55
|
+
const stat = lstatSync(file, { throwIfNoEntry: false });
|
|
56
|
+
if (!stat) continue; // Rotation may remove an entry during enumeration.
|
|
57
|
+
if (!stat.isFile() || stat.nlink !== 1) throw unsafe();
|
|
58
|
+
if (
|
|
59
|
+
!newest ||
|
|
60
|
+
stat.mtimeMs > newest.mtime ||
|
|
61
|
+
(stat.mtimeMs === newest.mtime &&
|
|
62
|
+
file.localeCompare(newest.file, undefined, { numeric: true }) > 0)
|
|
63
|
+
) {
|
|
64
|
+
newest = { file, mtime: stat.mtimeMs };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
} finally {
|
|
68
|
+
handle.closeSync();
|
|
69
|
+
}
|
|
70
|
+
return newest?.file;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function tailLogs(options: TailOptions): Tail {
|
|
74
|
+
if (
|
|
75
|
+
!/^[A-Za-z0-9_-][A-Za-z0-9_.-]{0,127}$/.test(options.prefix) ||
|
|
76
|
+
!Number.isInteger(options.tail) ||
|
|
77
|
+
options.tail < 0 ||
|
|
78
|
+
options.tail > 1000
|
|
79
|
+
)
|
|
80
|
+
throw unsafe();
|
|
81
|
+
// Validate before the caller commits SSE headers. Later errors are signaled.
|
|
82
|
+
try {
|
|
83
|
+
newestFile(options.dir, options.prefix);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
if (error instanceof HttpError) throw error;
|
|
86
|
+
throw new HttpError(503, "logsUnavailable", {
|
|
87
|
+
code: "LOG_UNAVAILABLE",
|
|
88
|
+
defaultMessage: "Log files are unavailable",
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
let closed = false;
|
|
92
|
+
let reading = false;
|
|
93
|
+
let first = true;
|
|
94
|
+
let identity = "";
|
|
95
|
+
let offset = 0;
|
|
96
|
+
let totalRead = 0;
|
|
97
|
+
let pending = Buffer.alloc(0);
|
|
98
|
+
let dropping = false;
|
|
99
|
+
let active: FileHandle | undefined;
|
|
100
|
+
let resolveReady!: () => void;
|
|
101
|
+
const ready = new Promise<void>((resolve) => {
|
|
102
|
+
resolveReady = resolve;
|
|
103
|
+
});
|
|
104
|
+
const close = () => {
|
|
105
|
+
if (closed) return;
|
|
106
|
+
closed = true;
|
|
107
|
+
clearInterval(timer);
|
|
108
|
+
clearImmediate(initial);
|
|
109
|
+
pending = Buffer.alloc(0);
|
|
110
|
+
void active?.close().catch(() => {});
|
|
111
|
+
resolveReady();
|
|
112
|
+
};
|
|
113
|
+
const fail = (code: "LOG_UNAVAILABLE" | "LOG_READ_LIMIT") => {
|
|
114
|
+
close();
|
|
115
|
+
try {
|
|
116
|
+
options.onError?.(code);
|
|
117
|
+
} catch {
|
|
118
|
+
/* Never reject a background task. */
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
const emit = async (line: string) => {
|
|
122
|
+
if (!closed && options.onLine(line) === false) close();
|
|
123
|
+
// Let a healthy socket flush between history records. A false return still
|
|
124
|
+
// stops immediately, and only one yield/read can be outstanding per tail.
|
|
125
|
+
if (!closed) await new Promise<void>((resolve) => setImmediate(resolve));
|
|
126
|
+
};
|
|
127
|
+
const consume = async (buffer: Buffer, history: boolean) => {
|
|
128
|
+
const lines: string[] = [];
|
|
129
|
+
let start = 0;
|
|
130
|
+
while (start < buffer.length && !closed) {
|
|
131
|
+
const newline = buffer.indexOf(10, start);
|
|
132
|
+
const end = newline === -1 ? buffer.length : newline;
|
|
133
|
+
const part = buffer.subarray(start, end);
|
|
134
|
+
if (!dropping) {
|
|
135
|
+
if (pending.length + part.length > LOG_TAIL_LIMITS.lineBytes) {
|
|
136
|
+
pending = Buffer.alloc(0);
|
|
137
|
+
dropping = true;
|
|
138
|
+
} else pending = Buffer.concat([pending, part]);
|
|
139
|
+
}
|
|
140
|
+
if (newline === -1) break;
|
|
141
|
+
if (!dropping) {
|
|
142
|
+
const line = pending.toString("utf8").replace(/\r$/, "");
|
|
143
|
+
if (line.trim()) {
|
|
144
|
+
if (history) {
|
|
145
|
+
lines.push(line);
|
|
146
|
+
if (lines.length > options.tail) lines.shift();
|
|
147
|
+
} else await emit(line);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
pending = Buffer.alloc(0);
|
|
151
|
+
dropping = false;
|
|
152
|
+
start = newline + 1;
|
|
153
|
+
}
|
|
154
|
+
for (const line of lines) {
|
|
155
|
+
if (closed) break;
|
|
156
|
+
await emit(line);
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
const pump = async () => {
|
|
160
|
+
if (closed || reading) return;
|
|
161
|
+
reading = true;
|
|
162
|
+
let file: FileHandle | undefined;
|
|
163
|
+
try {
|
|
164
|
+
const newest = newestFile(options.dir, options.prefix);
|
|
165
|
+
if (!newest) {
|
|
166
|
+
first = false;
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
file = await open(
|
|
170
|
+
newest,
|
|
171
|
+
constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK,
|
|
172
|
+
);
|
|
173
|
+
if (closed) return;
|
|
174
|
+
active = file;
|
|
175
|
+
const stat = await file.stat();
|
|
176
|
+
if (closed) return;
|
|
177
|
+
if (
|
|
178
|
+
!stat.isFile() ||
|
|
179
|
+
stat.nlink !== 1 ||
|
|
180
|
+
!Number.isSafeInteger(stat.size)
|
|
181
|
+
)
|
|
182
|
+
throw unsafe();
|
|
183
|
+
const nextIdentity = stat.dev + ":" + stat.ino;
|
|
184
|
+
if (identity !== nextIdentity || stat.size < offset) {
|
|
185
|
+
offset = 0;
|
|
186
|
+
pending = Buffer.alloc(0);
|
|
187
|
+
dropping = false;
|
|
188
|
+
identity = nextIdentity;
|
|
189
|
+
}
|
|
190
|
+
const history = first;
|
|
191
|
+
first = false;
|
|
192
|
+
if (history) {
|
|
193
|
+
offset = options.tail
|
|
194
|
+
? Math.max(0, stat.size - LOG_TAIL_LIMITS.historyBytes)
|
|
195
|
+
: stat.size;
|
|
196
|
+
dropping = offset > 0; // Discard a possible fragment at the window start.
|
|
197
|
+
if (!options.tail && stat.size) {
|
|
198
|
+
const last = Buffer.alloc(1);
|
|
199
|
+
await file.read(last, 0, 1, stat.size - 1);
|
|
200
|
+
totalRead++;
|
|
201
|
+
dropping = last[0] !== 10;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const size = Math.min(
|
|
205
|
+
stat.size - offset,
|
|
206
|
+
history ? LOG_TAIL_LIMITS.historyBytes : LOG_TAIL_LIMITS.chunkBytes,
|
|
207
|
+
);
|
|
208
|
+
if (size <= 0 || closed) return;
|
|
209
|
+
if (totalRead + size > LOG_TAIL_LIMITS.readBytes) {
|
|
210
|
+
fail("LOG_READ_LIMIT");
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
const buffer = Buffer.alloc(size);
|
|
214
|
+
let bytesRead = 0;
|
|
215
|
+
while (bytesRead < size && !closed) {
|
|
216
|
+
const part = await file.read(
|
|
217
|
+
buffer,
|
|
218
|
+
bytesRead,
|
|
219
|
+
size - bytesRead,
|
|
220
|
+
offset + bytesRead,
|
|
221
|
+
);
|
|
222
|
+
if (!part.bytesRead) break;
|
|
223
|
+
bytesRead += part.bytesRead;
|
|
224
|
+
}
|
|
225
|
+
if (closed) return;
|
|
226
|
+
offset += bytesRead;
|
|
227
|
+
totalRead += bytesRead;
|
|
228
|
+
await consume(buffer.subarray(0, bytesRead), history);
|
|
229
|
+
} catch (error) {
|
|
230
|
+
if (
|
|
231
|
+
!closed &&
|
|
232
|
+
!(error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
233
|
+
)
|
|
234
|
+
fail("LOG_UNAVAILABLE");
|
|
235
|
+
} finally {
|
|
236
|
+
if (file) await file.close().catch(() => {});
|
|
237
|
+
active = undefined;
|
|
238
|
+
reading = false;
|
|
239
|
+
resolveReady();
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
// A fixed poll bounds work under write storms; there is no event-driven read loop.
|
|
243
|
+
const timer = setInterval(() => void pump(), LOG_TAIL_LIMITS.pollMs);
|
|
244
|
+
timer.unref();
|
|
245
|
+
const initial = setImmediate(() => void pump());
|
|
246
|
+
return {
|
|
247
|
+
ready,
|
|
248
|
+
get closed() {
|
|
249
|
+
return closed;
|
|
250
|
+
},
|
|
251
|
+
close,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { AclBuilder } from "@tulipes/core/acl";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The log stream is `sysadmin`-only, like the console's writes.
|
|
5
|
+
*
|
|
6
|
+
* That boundary is real rather than nominal because the auth module never
|
|
7
|
+
* grants `admin` the `*` wildcard — a wildcard would reach these ids like
|
|
8
|
+
* any other, and "sysadmin only" would have been a label rather than a rule.
|
|
9
|
+
*/
|
|
10
|
+
export default function settingsAcl(acl: AclBuilder): void {
|
|
11
|
+
acl.allow("sysadmin", "settings:*");
|
|
12
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@app/settings",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"tulipes": {
|
|
7
|
+
"tier": "app",
|
|
8
|
+
"priority": 900,
|
|
9
|
+
"dependsOn": [
|
|
10
|
+
"auth",
|
|
11
|
+
"core"
|
|
12
|
+
]
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@app/auth": "workspace:*",
|
|
16
|
+
"@app/core": "workspace:*",
|
|
17
|
+
"@tulipes/core": "workspace:*",
|
|
18
|
+
"express": "^5",
|
|
19
|
+
"zod": "^3.25"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Router } from "express";
|
|
2
|
+
import type { BackendCtx } from "@tulipes/core/boot";
|
|
3
|
+
|
|
4
|
+
import { SettingsController } from "../controllers/settings.controllers.ts";
|
|
5
|
+
import { logQuery } from "../schemas/settings.schemas.ts";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* One endpoint: the log stream. Everything the console used to do here —
|
|
9
|
+
* describing modules, editing variables and translations, restarting —
|
|
10
|
+
* is either a setting now (the `config` module edits those, hot, with a
|
|
11
|
+
* version and an audit) or code in the repository.
|
|
12
|
+
*/
|
|
13
|
+
export default function settingsRoutes(ctx: BackendCtx): void {
|
|
14
|
+
const { rai, routes, config } = ctx;
|
|
15
|
+
const router = Router();
|
|
16
|
+
const settings = new SettingsController(ctx);
|
|
17
|
+
|
|
18
|
+
router.get(
|
|
19
|
+
"/logs/stream",
|
|
20
|
+
rai({
|
|
21
|
+
id: "settings:logs",
|
|
22
|
+
name: "Stream logs",
|
|
23
|
+
description: "Server-Sent Events over the log files — backend or worker",
|
|
24
|
+
folder: "settings",
|
|
25
|
+
query: logQuery,
|
|
26
|
+
// A stream is not a tool: it never ends on its own.
|
|
27
|
+
agent: false,
|
|
28
|
+
}),
|
|
29
|
+
settings.logStream(),
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
routes.mount(router, `${config.api!.prefix}/settings`);
|
|
33
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { z } from "zod/v4";
|
|
2
|
+
|
|
3
|
+
/** Validated by rai(), and described in the generated spec. */
|
|
4
|
+
export const logQuery = z.object({
|
|
5
|
+
process: z.enum(["backend", "worker"]).optional(),
|
|
6
|
+
level: z.enum(["trace", "debug", "info", "warn", "error", "fatal"]).optional(),
|
|
7
|
+
tail: z.coerce.number().int().min(0).max(1000).optional(),
|
|
8
|
+
});
|