agent-standup 0.20.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.
@@ -0,0 +1,407 @@
1
+ #!/usr/bin/env node
2
+
3
+ // scripts/lib/run-migrations.mjs
4
+ import { spawn } from "node:child_process";
5
+ var isWindows = process.platform === "win32";
6
+ async function runMigrations({
7
+ cwd = process.cwd(),
8
+ env = process.env,
9
+ log = console,
10
+ schemaPath
11
+ } = {}) {
12
+ log.info('Applying database migrations ("prisma migrate deploy")...');
13
+ const args = ["prisma", "migrate", "deploy"];
14
+ if (schemaPath) {
15
+ args.push("--schema", schemaPath);
16
+ }
17
+ const exitCode = await new Promise((resolve, reject) => {
18
+ const child = spawn(isWindows ? "npx.cmd" : "npx", args, {
19
+ cwd,
20
+ env,
21
+ stdio: "inherit",
22
+ shell: isWindows
23
+ });
24
+ child.on("error", reject);
25
+ child.on("exit", (code) => resolve(code ?? 1));
26
+ });
27
+ if (exitCode !== 0) {
28
+ log.error(
29
+ `FATAL: database migration failed ("prisma migrate deploy" exited ${exitCode}) \u2014 refusing to start the application. See the Prisma output above for details.`
30
+ );
31
+ return { ok: false, exitCode };
32
+ }
33
+ log.info("Migrations applied successfully.");
34
+ return { ok: true, exitCode: 0 };
35
+ }
36
+
37
+ // scripts/lib/verify-round-trip.mjs
38
+ import { PrismaClient } from "@prisma/client";
39
+ var RoundTripFailedError = class extends Error {
40
+ constructor(message, options) {
41
+ super(message, options);
42
+ this.name = "RoundTripFailedError";
43
+ }
44
+ };
45
+ async function verifyRoundTrip({ databaseUrl, log = console }) {
46
+ if (!databaseUrl) {
47
+ throw new RoundTripFailedError("No connection string was given to verify.");
48
+ }
49
+ const prisma = new PrismaClient({ datasourceUrl: databaseUrl });
50
+ const marker = `standup-init-${Date.now()}-${Math.random().toString(36).slice(2)}`;
51
+ try {
52
+ await prisma.$executeRawUnsafe(
53
+ `CREATE TEMP TABLE "standup_init_round_trip" ("value" text NOT NULL)`
54
+ );
55
+ await prisma.$executeRawUnsafe(
56
+ `INSERT INTO "standup_init_round_trip" ("value") VALUES ($1)`,
57
+ marker
58
+ );
59
+ const rows = await prisma.$queryRawUnsafe(`SELECT "value" FROM "standup_init_round_trip"`);
60
+ if (!Array.isArray(rows) || rows.length !== 1 || rows[0]?.value !== marker) {
61
+ throw new RoundTripFailedError(
62
+ "The value read back did not match the value written \u2014 the connection accepted the write but did not return it faithfully."
63
+ );
64
+ }
65
+ log.info("Live round trip succeeded: wrote a row and read it back unchanged.");
66
+ } catch (cause) {
67
+ if (cause instanceof RoundTripFailedError) throw cause;
68
+ throw new RoundTripFailedError("Could not complete a write-then-read against the database.", {
69
+ cause
70
+ });
71
+ } finally {
72
+ await prisma.$disconnect().catch(() => {
73
+ });
74
+ }
75
+ }
76
+
77
+ // scripts/lib/provision-db.mjs
78
+ import { spawnSync } from "node:child_process";
79
+ import { randomBytes } from "node:crypto";
80
+ var isWindows2 = process.platform === "win32";
81
+ var SAFE_IDENTIFIER = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
82
+ var InvalidIdentifierError = class extends Error {
83
+ constructor(kind, value) {
84
+ super(
85
+ `${kind} ${JSON.stringify(value)} is not a safe identifier \u2014 only letters, digits and underscore, not starting with a digit, are accepted.`
86
+ );
87
+ this.name = "InvalidIdentifierError";
88
+ }
89
+ };
90
+ function assertSafeIdentifier(kind, value) {
91
+ if (typeof value !== "string" || !SAFE_IDENTIFIER.test(value)) {
92
+ throw new InvalidIdentifierError(kind, value);
93
+ }
94
+ return value;
95
+ }
96
+ function quoteLiteral(value) {
97
+ return `'${String(value).replace(/'/g, "''")}'`;
98
+ }
99
+ function generatePassword() {
100
+ return randomBytes(24).toString("hex");
101
+ }
102
+ function withDatabaseName(url, name) {
103
+ const parsed = new URL(url);
104
+ parsed.pathname = `/${name}`;
105
+ return parsed.toString();
106
+ }
107
+ function buildAppUrl(provisionUrl, { databaseName, appRole, appPassword }) {
108
+ const parsed = new URL(provisionUrl);
109
+ parsed.username = encodeURIComponent(appRole);
110
+ parsed.password = encodeURIComponent(appPassword);
111
+ parsed.pathname = `/${databaseName}`;
112
+ parsed.search = "";
113
+ return parsed.toString();
114
+ }
115
+ function execute(url, sql) {
116
+ const result = spawnSync(
117
+ isWindows2 ? "npx.cmd" : "npx",
118
+ ["prisma", "db", "execute", "--url", url, "--stdin"],
119
+ { input: sql, encoding: "utf-8", shell: isWindows2 }
120
+ );
121
+ if (result.status === 0) return { ok: true };
122
+ const output = `${result.stderr ?? ""}${result.stdout ?? ""}`;
123
+ const alreadyExists = /already exists/i.test(output);
124
+ return { ok: false, alreadyExists, output };
125
+ }
126
+ function ensureDatabase(provisionUrl, databaseName, log = console) {
127
+ assertSafeIdentifier("database name", databaseName);
128
+ const adminUrl = withDatabaseName(provisionUrl, "postgres");
129
+ const result = execute(adminUrl, `CREATE DATABASE "${databaseName}";`);
130
+ if (result.ok) {
131
+ log.info(`Created database "${databaseName}".`);
132
+ return;
133
+ }
134
+ if (result.alreadyExists) {
135
+ log.info(`Database "${databaseName}" already exists; leaving it as it is.`);
136
+ return;
137
+ }
138
+ throw new Error(`Could not create database "${databaseName}" (see prisma's own output above).`);
139
+ }
140
+ function ensureAppRole(provisionUrl, appRole, appPassword, log = console) {
141
+ assertSafeIdentifier("application role", appRole);
142
+ const adminUrl = withDatabaseName(provisionUrl, "postgres");
143
+ const sql = `
144
+ DO $$
145
+ BEGIN
146
+ IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = ${quoteLiteral(appRole)}) THEN
147
+ CREATE ROLE "${appRole}" WITH LOGIN PASSWORD ${quoteLiteral(appPassword)};
148
+ ELSE
149
+ ALTER ROLE "${appRole}" WITH LOGIN PASSWORD ${quoteLiteral(appPassword)};
150
+ END IF;
151
+ END
152
+ $$;`;
153
+ const result = execute(adminUrl, sql);
154
+ if (!result.ok) {
155
+ throw new Error(
156
+ `Could not create or update role "${appRole}" (see prisma's own output above).`
157
+ );
158
+ }
159
+ log.info(`Application role "${appRole}" is ready.`);
160
+ }
161
+ function grantAppRole(provisionUrl, databaseName, appRole, log = console) {
162
+ assertSafeIdentifier("database name", databaseName);
163
+ assertSafeIdentifier("application role", appRole);
164
+ const targetUrl = withDatabaseName(provisionUrl, databaseName);
165
+ const statements = [
166
+ `GRANT CONNECT ON DATABASE "${databaseName}" TO "${appRole}";`,
167
+ `GRANT USAGE ON SCHEMA public TO "${appRole}";`,
168
+ `GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO "${appRole}";`,
169
+ `GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO "${appRole}";`,
170
+ `ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO "${appRole}";`,
171
+ `ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE, SELECT ON SEQUENCES TO "${appRole}";`
172
+ ];
173
+ for (const sql of statements) {
174
+ const result = execute(targetUrl, sql);
175
+ if (!result.ok) {
176
+ throw new Error(
177
+ `Could not grant privileges to role "${appRole}" (see prisma's own output above).`
178
+ );
179
+ }
180
+ }
181
+ log.info(`Granted "${appRole}" read/write on "${databaseName}".`);
182
+ }
183
+ function provisionAppDatabase({
184
+ provisionUrl,
185
+ databaseName,
186
+ appRole,
187
+ appPassword,
188
+ log = console
189
+ }) {
190
+ const password = appPassword ?? generatePassword();
191
+ ensureDatabase(provisionUrl, databaseName, log);
192
+ ensureAppRole(provisionUrl, appRole, password, log);
193
+ grantAppRole(provisionUrl, databaseName, appRole, log);
194
+ return {
195
+ appUrl: buildAppUrl(provisionUrl, { databaseName, appRole, appPassword: password }),
196
+ migrateUrl: withDatabaseName(provisionUrl, databaseName),
197
+ generatedPassword: appPassword === void 0
198
+ };
199
+ }
200
+
201
+ // scripts/lib/container-provision.mjs
202
+ import { spawn as spawn2, spawnSync as spawnSync2 } from "node:child_process";
203
+
204
+ // scripts/lib/boot-env.mjs
205
+ var DEFAULT_DB_WAIT_TIMEOUT_SECONDS = 180;
206
+ var DEFAULT_DB_WAIT_INTERVAL_SECONDS = 2;
207
+
208
+ // scripts/lib/wait-for-db.mjs
209
+ import { PrismaClient as PrismaClient2 } from "@prisma/client";
210
+ var DatabaseUnreachableError = class extends Error {
211
+ constructor(message, options) {
212
+ super(message, options);
213
+ this.name = "DatabaseUnreachableError";
214
+ }
215
+ };
216
+ function sleep(ms) {
217
+ return new Promise((resolve) => setTimeout(resolve, ms));
218
+ }
219
+ function summarize(value) {
220
+ return String(value?.message ?? value).split("\n").map((line) => line.trim()).filter(Boolean).join(" ");
221
+ }
222
+ async function waitForDatabase({
223
+ databaseUrl,
224
+ timeoutMs = DEFAULT_DB_WAIT_TIMEOUT_SECONDS * 1e3,
225
+ intervalMs = DEFAULT_DB_WAIT_INTERVAL_SECONDS * 1e3,
226
+ log = console
227
+ } = {}) {
228
+ if (!databaseUrl) {
229
+ throw new DatabaseUnreachableError("DATABASE_URL is not set.");
230
+ }
231
+ const deadline = Date.now() + timeoutMs;
232
+ let lastError;
233
+ let attempt = 0;
234
+ for (; ; ) {
235
+ attempt += 1;
236
+ const probe = new PrismaClient2({ datasourceUrl: databaseUrl });
237
+ try {
238
+ await probe.$queryRaw`SELECT 1`;
239
+ await probe.$disconnect();
240
+ log.info(`Database reachable after ${attempt} attempt(s).`);
241
+ return;
242
+ } catch (err) {
243
+ lastError = err;
244
+ await probe.$disconnect().catch(() => {
245
+ });
246
+ const remaining = deadline - Date.now();
247
+ if (!Number.isFinite(remaining) || remaining <= 0) {
248
+ log.warn(`Database still not ready (attempt ${attempt}: ${summarize(err)}); giving up.`);
249
+ break;
250
+ }
251
+ log.warn(`Database not ready yet (attempt ${attempt}: ${summarize(err)}); retrying...`);
252
+ await sleep(Math.min(intervalMs, remaining));
253
+ }
254
+ }
255
+ throw new DatabaseUnreachableError(
256
+ `Database still unreachable after ${timeoutMs}ms across ${attempt} attempt(s): ${summarize(lastError)}`,
257
+ { cause: lastError }
258
+ );
259
+ }
260
+
261
+ // scripts/lib/container-provision.mjs
262
+ var isWindows3 = process.platform === "win32";
263
+ var DEV_COMPOSE_PROVISION_URL = "postgres://standup:standup@localhost:5433/postgres";
264
+ function dockerAvailable() {
265
+ const result = spawnSync2(isWindows3 ? "docker.exe" : "docker", ["compose", "version"], {
266
+ encoding: "utf-8",
267
+ shell: isWindows3
268
+ });
269
+ return result.status === 0;
270
+ }
271
+ async function attemptContainerProvision({
272
+ cwd = process.cwd(),
273
+ log = console,
274
+ timeoutMs = DEFAULT_DB_WAIT_TIMEOUT_SECONDS * 1e3,
275
+ intervalMs = DEFAULT_DB_WAIT_INTERVAL_SECONDS * 1e3
276
+ } = {}) {
277
+ if (!dockerAvailable()) {
278
+ return { ok: false, reason: "No container runtime (`docker compose`) is available." };
279
+ }
280
+ const upExitCode = await new Promise((resolve) => {
281
+ const child = spawn2(isWindows3 ? "docker.exe" : "docker", ["compose", "up", "-d", "db"], {
282
+ cwd,
283
+ stdio: "inherit",
284
+ shell: isWindows3
285
+ });
286
+ child.on("error", () => resolve(1));
287
+ child.on("exit", (code) => resolve(code ?? 1));
288
+ });
289
+ if (upExitCode !== 0) {
290
+ return { ok: false, reason: "`docker compose up -d db` did not succeed." };
291
+ }
292
+ try {
293
+ await waitForDatabase({
294
+ databaseUrl: DEV_COMPOSE_PROVISION_URL,
295
+ timeoutMs,
296
+ intervalMs,
297
+ log
298
+ });
299
+ } catch (err) {
300
+ if (err instanceof DatabaseUnreachableError) {
301
+ return { ok: false, reason: "The container started but Postgres never became reachable." };
302
+ }
303
+ throw err;
304
+ }
305
+ return { ok: true, provisionUrl: DEV_COMPOSE_PROVISION_URL };
306
+ }
307
+
308
+ // scripts/lib/run-init.mjs
309
+ import { spawn as spawn3 } from "node:child_process";
310
+ function runSeed({ databaseUrl, cwd, env, log }) {
311
+ return new Promise((resolve) => {
312
+ log.info("Seeding reference data...");
313
+ const child = spawn3("node", ["prisma/seed.mjs"], {
314
+ cwd,
315
+ env: { ...env, DATABASE_URL: databaseUrl },
316
+ stdio: "inherit"
317
+ });
318
+ child.on("error", () => resolve({ ok: false }));
319
+ child.on("exit", (code) => resolve({ ok: (code ?? 1) === 0 }));
320
+ });
321
+ }
322
+ function describeDatabase(databaseUrl) {
323
+ const url = new URL(databaseUrl);
324
+ return {
325
+ host: url.hostname,
326
+ port: url.port || void 0,
327
+ name: url.pathname.replace(/^\//, "")
328
+ };
329
+ }
330
+ async function runInitSequence({
331
+ source,
332
+ cwd = process.cwd(),
333
+ env = process.env,
334
+ log = console
335
+ }) {
336
+ let appUrl;
337
+ let migrateUrl;
338
+ let resultSource;
339
+ let appRole;
340
+ if (source.kind === "accept") {
341
+ appUrl = source.databaseUrl;
342
+ migrateUrl = source.databaseUrl;
343
+ resultSource = "accepted";
344
+ } else {
345
+ let provisionUrl = source.kind === "provision" ? source.provisionUrl : void 0;
346
+ if (source.kind === "auto") {
347
+ const attempt = await attemptContainerProvision({ cwd, log });
348
+ if (!attempt.ok) {
349
+ return {
350
+ ok: false,
351
+ stage: "container",
352
+ message: `Could not find or provision a database automatically (${attempt.reason}). Supply one with --database-url (a connection string to use directly) or --provision-url (an admin connection to provision a fresh database and role from), then run \`standup init\` again.`
353
+ };
354
+ }
355
+ provisionUrl = attempt.provisionUrl;
356
+ }
357
+ try {
358
+ const provisioned = provisionAppDatabase({
359
+ provisionUrl,
360
+ databaseName: source.databaseName,
361
+ appRole: source.appRole,
362
+ appPassword: source.appPassword,
363
+ log
364
+ });
365
+ appUrl = provisioned.appUrl;
366
+ migrateUrl = provisioned.migrateUrl;
367
+ } catch (cause) {
368
+ log.error("Provisioning the database and application role failed.", cause);
369
+ return {
370
+ ok: false,
371
+ stage: "provision",
372
+ message: "Could not provision the database and application role (see the output above)."
373
+ };
374
+ }
375
+ resultSource = source.kind === "auto" ? "provisioned-via-container" : "provisioned";
376
+ appRole = source.appRole;
377
+ }
378
+ const migration = await runMigrations({ env: { ...env, DATABASE_URL: migrateUrl }, cwd, log });
379
+ if (!migration.ok) {
380
+ return { ok: false, stage: "migrate", message: "Migrations did not apply cleanly." };
381
+ }
382
+ const seeded = await runSeed({ databaseUrl: appUrl, cwd, env, log });
383
+ if (!seeded.ok) {
384
+ return { ok: false, stage: "seed", message: "Seeding reference data failed." };
385
+ }
386
+ try {
387
+ await verifyRoundTrip({ databaseUrl: appUrl, log });
388
+ } catch {
389
+ return {
390
+ ok: false,
391
+ stage: "verify",
392
+ message: "Migrated and seeded, but the live round trip (write, then read it back) failed."
393
+ };
394
+ }
395
+ return {
396
+ ok: true,
397
+ databaseUrl: appUrl,
398
+ source: resultSource,
399
+ database: describeDatabase(appUrl),
400
+ ...appRole === void 0 ? {} : { appRole },
401
+ steps: { migrated: true, seeded: true, verified: true }
402
+ };
403
+ }
404
+ export {
405
+ describeDatabase,
406
+ runInitSequence
407
+ };
package/package.json ADDED
@@ -0,0 +1,88 @@
1
+ {
2
+ "name": "agent-standup",
3
+ "version": "0.20.0",
4
+ "private": false,
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "standup": "dist/bin/standup.js",
9
+ "standup-hook": "dist/bin/standup-hook.js"
10
+ },
11
+ "files": [
12
+ "dist"
13
+ ],
14
+ "engines": {
15
+ "node": ">=24"
16
+ },
17
+ "scripts": {
18
+ "prepare": "node scripts/install-git-hooks.mjs",
19
+ "prepack": "npm run build:cli",
20
+ "dev": "next dev",
21
+ "build": "next build",
22
+ "build:cli": "node scripts/build-cli.mjs",
23
+ "build:plugin": "node scripts/build-plugin.mjs",
24
+ "start": "next start",
25
+ "typecheck": "tsc --noEmit",
26
+ "lint": "eslint .",
27
+ "format": "prettier --write .",
28
+ "format:check": "prettier --check .",
29
+ "test": "node scripts/run-tests.mjs",
30
+ "test:watch": "vitest",
31
+ "check:external-refs": "node scripts/check-external-refs.mjs",
32
+ "generate:http-routes": "node scripts/generate-http-routes.mjs",
33
+ "check:http-routes": "node scripts/generate-http-routes.mjs --check",
34
+ "check:db-imports": "node scripts/check-db-import-allowlist.mjs",
35
+ "check:updater-side-effects": "node scripts/check-updater-side-effects.mjs",
36
+ "check:event-emitters": "node scripts/check-event-emitters.mjs",
37
+ "check:hook-freshness": "node scripts/check-hook-freshness.mjs",
38
+ "check:db-gated": "node scripts/check-db-gated-suites.mjs",
39
+ "check:db-gated:require": "node scripts/check-db-gated-suites.mjs --require-db",
40
+ "version:from-tag": "node scripts/version-from-tag.mjs",
41
+ "db:up": "node scripts/db-up.mjs",
42
+ "db:migrate": "prisma migrate dev",
43
+ "db:deploy": "prisma migrate deploy",
44
+ "db:studio": "prisma studio",
45
+ "db:check-drift": "node scripts/check-migration-drift.mjs",
46
+ "db:seed": "node prisma/seed.mjs",
47
+ "backfill": "node scripts/backfill.mjs",
48
+ "test:raw": "vitest run"
49
+ },
50
+ "dependencies": {
51
+ "@dnd-kit/core": "^6.3.1",
52
+ "@dnd-kit/modifiers": "^9.0.0",
53
+ "@modelcontextprotocol/sdk": "^1.12.0",
54
+ "@prisma/client": "^6.0.0",
55
+ "@radix-ui/react-slot": "^1.3.3",
56
+ "@tailwindcss/postcss": "^4.3.3",
57
+ "clsx": "^2.1.1",
58
+ "geist": "^1.7.2",
59
+ "lucide-react": "^1.32.0",
60
+ "next": "^16.0.0",
61
+ "prisma": "^6.0.0",
62
+ "react": "^19.0.0",
63
+ "react-dom": "^19.0.0",
64
+ "react-markdown": "^10.1.0",
65
+ "remark-gfm": "^4.0.1",
66
+ "tailwind-merge": "^3.6.0",
67
+ "tailwindcss": "^4.3.3",
68
+ "zod": "^3.24.0"
69
+ },
70
+ "overrides": {
71
+ "qs": "^6.15.2",
72
+ "deepmerge-ts": "^8.0.2"
73
+ },
74
+ "devDependencies": {
75
+ "@types/node": "^24.0.0",
76
+ "@types/pg": "^8.21.0",
77
+ "@types/react": "^19.0.0",
78
+ "@types/react-dom": "^19.0.0",
79
+ "esbuild": "^0.28.2",
80
+ "eslint": "^9.0.0",
81
+ "eslint-config-next": "^16.0.0",
82
+ "jsdom": "^26.1.0",
83
+ "pg": "^8.23.0",
84
+ "prettier": "^3.0.0",
85
+ "typescript": "^5.7.0",
86
+ "vitest": "^4.0.0"
87
+ }
88
+ }