@zaip-ai/cli 1.3.2 → 1.3.4
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/LICENSE +6 -3
- package/README.md +8 -2
- package/dist/services/codex-pre-tool-hook-template.js +1 -1
- package/dist/services/runtime-template.test.js +9 -8
- package/dist/services/runtime-template.test.js.map +1 -1
- package/package.json +2 -2
- package/templates/.agents/dashboard/.zaip-managed.json +401 -381
- package/templates/.agents/dashboard/apps/frontend/dist/assets/{CostCalculationGuidePage-ovxurkIk.js → CostCalculationGuidePage-BZWjXQB-.js} +1 -1
- package/templates/.agents/dashboard/apps/frontend/dist/assets/{DashboardHelpDialog-BXy6nti1.js → DashboardHelpDialog-D8qqeIPE.js} +30 -1
- package/templates/.agents/dashboard/apps/frontend/dist/assets/{PricingSettingsDialog-vSvvOHQp.js → PricingSettingsDialog-Cm12_A0v.js} +1 -1
- package/templates/.agents/dashboard/apps/frontend/dist/assets/{ProjectSettingsPage-zH758vfs.js → ProjectSettingsPage-DxO9F8B4.js} +1 -1
- package/templates/.agents/dashboard/apps/frontend/dist/assets/{TraceabilityWorkspace-CTH2wDs5.js → TraceabilityWorkspace-_x0twOcS.js} +1 -1
- package/templates/.agents/dashboard/apps/frontend/dist/assets/cost-monitor-Cpp8xFij.js +68 -0
- package/templates/.agents/dashboard/apps/frontend/dist/assets/{icons-vendor-B_lDDHn1.js → icons-vendor-CR4Fbf3g.js} +1 -1
- package/templates/.agents/dashboard/apps/frontend/dist/assets/index-ncC1rxap.js +2 -0
- package/templates/.agents/dashboard/apps/frontend/dist/assets/{react-vendor-B3TJrD4O.js → react-vendor-DRJQbKnp.js} +1 -1
- package/templates/.agents/dashboard/apps/frontend/dist/assets/{traceability-7f1j5wx3.js → traceability-CMrDYqjR.js} +1 -1
- package/templates/.agents/dashboard/apps/frontend/dist/index.html +5 -5
- package/templates/.agents/dashboard/apps/frontend/src/App.tsx +48 -3
- package/templates/.agents/dashboard/apps/frontend/src/AppRequestGuards.tsx +44 -0
- package/templates/.agents/dashboard/apps/frontend/src/cost-period-refresh.test.ts +92 -1
- package/templates/.agents/dashboard/apps/frontend/src/snapshot-coherence.test.ts +2 -2
- package/templates/.agents/dashboard/modules/cost-monitor/src/react/CostDeveloperAttributionPanel.test.ts +60 -0
- package/templates/.agents/dashboard/modules/cost-monitor/src/react/CostDeveloperAttributionPanel.tsx +26 -9
- package/templates/.agents/dashboard/modules/cost-monitor/src/react/CostMonitorNavigation.tsx +189 -0
- package/templates/.agents/dashboard/modules/cost-monitor/src/react/CostMonitorPage.test.ts +122 -2
- package/templates/.agents/dashboard/modules/cost-monitor/src/react/CostMonitorPage.tsx +70 -155
- package/templates/.agents/dashboard/modules/operations/src/react/export/ExcelExportAction.test.ts +59 -0
- package/templates/.agents/dashboard/modules/operations/src/react/export/ExcelExportAction.tsx +55 -2
- package/templates/.agents/dashboard/packages/local-runtime/dist/server.cjs +274 -246
- package/templates/.agents/dashboard/packages/local-runtime/dist/sqlite-read-model.cjs +255 -227
- package/templates/.agents/dashboard/packages/local-runtime/server.mjs +263 -235
- package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-cost-dimension-facts.test.ts +282 -0
- package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-cost-dimension-facts.ts +71 -0
- package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-read-model-launch-progress.ts +18 -0
- package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-read-model-stale-seed.ts +50 -0
- package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-read-model.test.ts +9 -6
- package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-read-model.ts +45 -73
- package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-snapshot-seed.test.ts +79 -2
- package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-work-item-index.test.ts +56 -0
- package/templates/.agents/dashboard/packages/local-runtime/start-dashboard-launch-page.mjs +26 -8
- package/templates/.agents/dashboard/packages/local-runtime/start-dashboard-launch-page.test.mjs +45 -4
- package/templates/.agents/dashboard/packages/local-runtime/start-dashboard-lifecycle.mjs +58 -0
- package/templates/.agents/dashboard/packages/local-runtime/start-dashboard-lifecycle.test.mjs +81 -0
- package/templates/.agents/dashboard/packages/local-runtime/start-dashboard.mjs +171 -71
- package/templates/.agents/dashboard/packages/local-runtime/start-dashboard.test.mjs +152 -11
- package/templates/.agents/dashboard/packages/windows-launcher/Program.cs +4 -9
- package/templates/.agents/dashboard/tests/e2e/playwright.config.ts +9 -1
- package/templates/.agents/dashboard/tests/e2e/specs/cost-monitor-period-transition.spec.ts +232 -0
- package/templates/.agents/dashboard/tests/e2e/specs/dashboard.spec.ts +26 -5
- package/templates/.agents/requirements/autonomous-assurance.md +1 -1
- package/templates/.agents/requirements/comparative-audit.md +1 -1
- package/templates/.agents/requirements/context-engine.md +1 -1
- package/templates/.agents/requirements/cost-observability.md +1 -1
- package/templates/.agents/requirements/dashboard-launch.md +1 -1
- package/templates/.agents/requirements/data-export.md +1 -1
- package/templates/.agents/requirements/design-system.md +1 -1
- package/templates/.agents/requirements/execution-engine.md +1 -1
- package/templates/.agents/requirements/kanban-dashboard.md +1 -1
- package/templates/.agents/requirements/model-orchestration.md +1 -1
- package/templates/.agents/requirements/public-distribution.md +1 -1
- package/templates/.agents/requirements/software-architecture.md +1 -1
- package/templates/.agents/requirements/strategic-improvements.md +1 -1
- package/templates/.agents/requirements/work-lifecycle.md +1 -1
- package/templates/.agents/requirements/zaip-strategic-95.md +1 -1
- package/templates/.agents/runtime/bin/zaip.mjs +7 -7
- package/templates/.agents/runtime/checksums.sha256 +3 -3
- package/templates/.agents/runtime/provenance.json +3 -3
- package/templates/.agents/runtime/sbom.cdx.json +1595 -3491
- package/templates/.agents/skills/zaip-skill-lifecycle-closure/SKILL.md +16 -0
- package/templates/.agents/skills/zaip-skill-reconcile-telemetry/scripts/reconcile-local-telemetry.mjs +19 -20
- package/templates/.agents/skills/zaip-skill-reconcile-telemetry/scripts/reconcile-local-telemetry.test.mjs +143 -0
- package/templates/.agents/skills/zaip-skill-reconcile-telemetry/scripts/reconcile-telemetry-lib.mjs +14 -0
- package/templates/.agents/skills/zaip-skill-reconcile-telemetry/scripts/reconcile-telemetry-remeasurement.mjs +3 -1
- package/templates/.agents/skills/zaip-skill-reconcile-telemetry/scripts/reconcile-telemetry-remeasurement.test.mjs +21 -0
- package/templates/.agents/skills/zaip-skill-reconcile-telemetry/scripts/source-size-telemetry/model-identity-reconciliation.mjs +3 -0
- package/templates/.agents/zaip-aios/guides/26-qualidade-e-release.md +29 -0
- package/templates/template-manifest.json +536 -932
- package/templates/.agents/dashboard/.tmp-run-list.json +0 -254
- package/templates/.agents/dashboard/apps/frontend/dist/assets/cost-monitor-DKwP1stV.js +0 -68
- package/templates/.agents/dashboard/apps/frontend/dist/assets/index-CuMcoUni.js +0 -2
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
awaitOwnedDashboardExit,
|
|
6
|
+
createPhaseTimer,
|
|
7
|
+
presentLaunchFailure,
|
|
8
|
+
} from "./start-dashboard-lifecycle.mjs";
|
|
9
|
+
|
|
10
|
+
describe("dashboard launcher lifecycle", () => {
|
|
11
|
+
it("reports the existing failure state and closes the preparation page", () => {
|
|
12
|
+
const calls = [];
|
|
13
|
+
const launchPage = {
|
|
14
|
+
setFailure: (failure) => calls.push(["failure", failure]),
|
|
15
|
+
closeAfterFailureObservation: () => calls.push(["close"]),
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
expect(
|
|
19
|
+
presentLaunchFailure(
|
|
20
|
+
launchPage,
|
|
21
|
+
new Error("boom"),
|
|
22
|
+
(message, limit) => `${limit}:${message}`,
|
|
23
|
+
),
|
|
24
|
+
).toBeNull();
|
|
25
|
+
expect(calls).toEqual([
|
|
26
|
+
[
|
|
27
|
+
"failure",
|
|
28
|
+
{
|
|
29
|
+
percent: 100,
|
|
30
|
+
phase: "Não foi possível preparar o dashboard",
|
|
31
|
+
detail: "900:boom",
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
["close"],
|
|
35
|
+
]);
|
|
36
|
+
expect(presentLaunchFailure(null, "unused", () => "unused")).toBeNull();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("waits for the child exit before clearing the owned session", async () => {
|
|
40
|
+
const child = new EventEmitter();
|
|
41
|
+
const processRef = new EventEmitter();
|
|
42
|
+
const calls = [];
|
|
43
|
+
child.kill = (signal) => calls.push(["kill", signal]);
|
|
44
|
+
const completion = awaitOwnedDashboardExit({
|
|
45
|
+
child,
|
|
46
|
+
currentSession: { dbPath: "C:/tmp/dashboard.sqlite" },
|
|
47
|
+
clearOwnedDashboardSession: async () => calls.push(["clear"]),
|
|
48
|
+
recordPortStatus: async (_session, status) => calls.push(["status", status]),
|
|
49
|
+
removeGeneratedReadModel: async (path) => calls.push(["remove", path]),
|
|
50
|
+
processRef,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
expect(calls).toEqual([]);
|
|
54
|
+
processRef.emit("SIGINT");
|
|
55
|
+
child.emit("exit", null);
|
|
56
|
+
|
|
57
|
+
await expect(completion).resolves.toBe(0);
|
|
58
|
+
expect(calls).toEqual([
|
|
59
|
+
["kill", "SIGINT"],
|
|
60
|
+
["clear"],
|
|
61
|
+
["status", "closed"],
|
|
62
|
+
["remove", "C:/tmp/dashboard.sqlite"],
|
|
63
|
+
]);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("records launcher phase timing with the same process identity", () => {
|
|
67
|
+
const entries = [];
|
|
68
|
+
createPhaseTimer((entry) => entries.push(JSON.parse(entry)), 42)("ready", 1_000, {
|
|
69
|
+
port: 4417,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
expect(entries).toEqual([
|
|
73
|
+
expect.objectContaining({
|
|
74
|
+
event: "dashboard_sqlite_phase",
|
|
75
|
+
phase: "ready",
|
|
76
|
+
launcher_pid: 42,
|
|
77
|
+
port: 4417,
|
|
78
|
+
}),
|
|
79
|
+
]);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
@@ -19,6 +19,12 @@ import process from "node:process";
|
|
|
19
19
|
import { fileURLToPath } from "node:url";
|
|
20
20
|
import { collectPreviousDashboardSessions } from "./start-dashboard-history.mjs";
|
|
21
21
|
import { createDashboardLaunchPage } from "./start-dashboard-launch-page.mjs";
|
|
22
|
+
import {
|
|
23
|
+
awaitOwnedDashboardExit,
|
|
24
|
+
createPhaseTimer,
|
|
25
|
+
presentLaunchFailure,
|
|
26
|
+
writeLauncherStatus,
|
|
27
|
+
} from "./start-dashboard-lifecycle.mjs";
|
|
22
28
|
import {
|
|
23
29
|
isProcessAlive,
|
|
24
30
|
stopOwnedDashboardProcess,
|
|
@@ -62,18 +68,66 @@ const healthPath = "/api/v2/health";
|
|
|
62
68
|
const lockTimeoutMs = 210_000;
|
|
63
69
|
const staleLockMs = 30_000;
|
|
64
70
|
const lockHeartbeatFloorMs = 10;
|
|
65
|
-
// A cold immutable seed can legitimately take ~176 s on a busy Windows
|
|
66
|
-
// volume. Keep the immediate preparation page and fresh-port handoff, but do
|
|
67
|
-
// not kill that one-time canonical build before it can populate the cache.
|
|
68
|
-
const startupTimeoutMs = 180_000;
|
|
69
71
|
const maxServerStartAttempts = 4;
|
|
70
72
|
const maxPortHistory = 50;
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
]
|
|
73
|
+
// Keep each build invalidation scoped to the artifact it can actually change.
|
|
74
|
+
// The runtime bundle includes the server-side dashboard modules, workspace
|
|
75
|
+
// packages, settings and build scripts. Browser-only changes never rebuild it.
|
|
76
|
+
export const dashboardBuildTargets = {
|
|
77
|
+
frontend: {
|
|
78
|
+
artifacts: [resolve(frontendRoot, "index.html")],
|
|
79
|
+
sourceRoots: [
|
|
80
|
+
resolve(workspaceRoot, ".agents/dashboard/apps/frontend/src"),
|
|
81
|
+
resolve(workspaceRoot, ".agents/dashboard/apps/frontend/public"),
|
|
82
|
+
resolve(workspaceRoot, ".agents/dashboard/apps/frontend/index.html"),
|
|
83
|
+
resolve(workspaceRoot, ".agents/dashboard/modules/operations/src"),
|
|
84
|
+
resolve(workspaceRoot, ".agents/dashboard/modules/cost-monitor/src"),
|
|
85
|
+
resolve(workspaceRoot, ".agents/dashboard/modules/traceability/src"),
|
|
86
|
+
resolve(workspaceRoot, ".agents/dashboard/modules/operations/src/contracts.ts"),
|
|
87
|
+
resolve(workspaceRoot, ".agents/dashboard/modules/cost-monitor/src/contracts.ts"),
|
|
88
|
+
resolve(workspaceRoot, ".agents/dashboard/modules/traceability/src/contracts.ts"),
|
|
89
|
+
resolve(workspaceRoot, ".agents/dashboard/packages/contracts/src"),
|
|
90
|
+
resolve(workspaceRoot, ".agents/dashboard/packages/ui/src"),
|
|
91
|
+
resolve(workspaceRoot, ".agents/dashboard/apps/frontend/vite.config.ts"),
|
|
92
|
+
resolve(workspaceRoot, ".agents/dashboard/apps/frontend/package.json"),
|
|
93
|
+
resolve(workspaceRoot, ".agents/dashboard/apps/frontend/tsconfig.json"),
|
|
94
|
+
resolve(workspaceRoot, "tsconfig.base.json"),
|
|
95
|
+
resolve(workspaceRoot, "pnpm-lock.yaml"),
|
|
96
|
+
resolve(workspaceRoot, "README.md"),
|
|
97
|
+
],
|
|
98
|
+
excludedSourcePaths: ["/server/"],
|
|
99
|
+
},
|
|
100
|
+
runtime: {
|
|
101
|
+
artifacts: [
|
|
102
|
+
sourceServer,
|
|
103
|
+
resolve(packageRoot, "dist/server.js"),
|
|
104
|
+
resolve(packageRoot, "dist/sqlite-read-model.cjs"),
|
|
105
|
+
installedServer,
|
|
106
|
+
],
|
|
107
|
+
sourceRoots: [
|
|
108
|
+
resolve(packageRoot, "src"),
|
|
109
|
+
resolve(packageRoot, "scripts"),
|
|
110
|
+
resolve(packageRoot, "package.json"),
|
|
111
|
+
resolve(packageRoot, "tsconfig.json"),
|
|
112
|
+
resolve(workspaceRoot, ".agents/dashboard/modules/operations/src"),
|
|
113
|
+
resolve(workspaceRoot, ".agents/dashboard/modules/cost-monitor/src"),
|
|
114
|
+
resolve(workspaceRoot, ".agents/dashboard/modules/traceability/src"),
|
|
115
|
+
resolve(workspaceRoot, ".agents/dashboard/packages/contracts/src"),
|
|
116
|
+
resolve(workspaceRoot, ".agents/dashboard/packages/data-export/src"),
|
|
117
|
+
resolve(workspaceRoot, "packages/core/src"),
|
|
118
|
+
resolve(workspaceRoot, "packages/costs/src"),
|
|
119
|
+
resolve(workspaceRoot, "packages/decisions/src"),
|
|
120
|
+
resolve(workspaceRoot, "packages/lifecycle-manifests/src"),
|
|
121
|
+
resolve(workspaceRoot, "packages/specs/src"),
|
|
122
|
+
resolve(workspaceRoot, "packages/tasks/src"),
|
|
123
|
+
resolve(workspaceRoot, "package.json"),
|
|
124
|
+
resolve(workspaceRoot, "pnpm-workspace.yaml"),
|
|
125
|
+
resolve(workspaceRoot, "pnpm-lock.yaml"),
|
|
126
|
+
resolve(workspaceRoot, "tsconfig.base.json"),
|
|
127
|
+
],
|
|
128
|
+
excludedSourcePaths: ["/react/"],
|
|
129
|
+
},
|
|
130
|
+
};
|
|
77
131
|
export function preferredPort(value = process.env.ZAIP_SERVER_PORT) {
|
|
78
132
|
const parsed = Number.parseInt(value ?? "", 10);
|
|
79
133
|
return Number.isInteger(parsed) && parsed > 0 && parsed <= 65_535 ? parsed : defaultPort;
|
|
@@ -263,25 +317,40 @@ export async function findReusableDashboardSession({
|
|
|
263
317
|
async function backgroundRefreshIsRunning() {
|
|
264
318
|
return refreshIsRunning(join(sessionDirectory, "dashboard-sqlite-refresh.json"), isProcessAlive);
|
|
265
319
|
}
|
|
266
|
-
async function latestModifiedAt(paths) {
|
|
320
|
+
export async function latestModifiedAt(paths, excludedSourcePaths = []) {
|
|
267
321
|
let latest = 0;
|
|
268
322
|
for (const path of paths) {
|
|
269
323
|
const details = await stat(path).catch(() => null);
|
|
270
324
|
if (details === null) continue;
|
|
271
|
-
|
|
272
|
-
|
|
325
|
+
if (!details.isDirectory()) {
|
|
326
|
+
if (isBuildInputPath(path, excludedSourcePaths)) latest = Math.max(latest, details.mtimeMs);
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
// Retain included directory mtimes so removing or renaming a source file
|
|
330
|
+
// after a build still invalidates its artifact. Excluded trees stay out.
|
|
331
|
+
if (isBuildInputPath(path, excludedSourcePaths)) latest = Math.max(latest, details.mtimeMs);
|
|
273
332
|
const entries = await readdir(path, { withFileTypes: true }).catch(() => []);
|
|
274
333
|
latest = Math.max(
|
|
275
334
|
latest,
|
|
276
335
|
await latestModifiedAt(
|
|
277
336
|
entries
|
|
278
|
-
.filter((entry) =>
|
|
337
|
+
.filter((entry) => isBuildInputPath(join(path, entry.name), excludedSourcePaths))
|
|
279
338
|
.map((entry) => join(path, entry.name)),
|
|
339
|
+
excludedSourcePaths,
|
|
280
340
|
),
|
|
281
341
|
);
|
|
282
342
|
}
|
|
283
343
|
return latest;
|
|
284
344
|
}
|
|
345
|
+
function isBuildInputPath(path, excludedSourcePaths) {
|
|
346
|
+
const normalized = path.replaceAll("\\", "/");
|
|
347
|
+
const name = normalized.slice(normalized.lastIndexOf("/") + 1);
|
|
348
|
+
if (new Set(["dist", "node_modules", ".turbo"]).has(name)) return false;
|
|
349
|
+
if (/\.(?:test|spec)\.[cm]?[jt]sx?$/.test(name)) return false;
|
|
350
|
+
return !excludedSourcePaths.some(
|
|
351
|
+
(excluded) => normalized.includes(excluded) || normalized.endsWith(excluded.slice(0, -1)),
|
|
352
|
+
);
|
|
353
|
+
}
|
|
285
354
|
async function runSqliteBuild(target) {
|
|
286
355
|
const frontend = target === "frontend";
|
|
287
356
|
const command = process.execPath;
|
|
@@ -320,17 +389,75 @@ async function runSqliteBuild(target) {
|
|
|
320
389
|
});
|
|
321
390
|
return output;
|
|
322
391
|
}
|
|
323
|
-
async function
|
|
324
|
-
const
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
392
|
+
export async function inspectDashboardBuildTarget(target) {
|
|
393
|
+
const artifactStats = await Promise.all(
|
|
394
|
+
target.artifacts.map(async (path) => ({ path, details: await stat(path).catch(() => null) })),
|
|
395
|
+
);
|
|
396
|
+
const missingArtifacts = artifactStats
|
|
397
|
+
.filter(({ details }) => details === null)
|
|
398
|
+
.map(({ path }) => path);
|
|
399
|
+
const artifactsModifiedAt = Math.min(
|
|
400
|
+
...artifactStats.map(({ details }) => details?.mtimeMs ?? 0),
|
|
401
|
+
);
|
|
402
|
+
const sourcesModifiedAt = await latestModifiedAt(target.sourceRoots, target.excludedSourcePaths);
|
|
403
|
+
return { artifactsModifiedAt, missingArtifacts, sourcesModifiedAt };
|
|
404
|
+
}
|
|
405
|
+
function buildProgress(onProgress, percent, phase, detail) {
|
|
406
|
+
onProgress({ percent, phase, detail });
|
|
407
|
+
}
|
|
408
|
+
export async function refreshDashboardBuildTargets({
|
|
409
|
+
targets = dashboardBuildTargets,
|
|
410
|
+
forceRebuild = process.env.ZAIP_DASHBOARD_FORCE_REBUILD === "1",
|
|
411
|
+
inspectTarget = inspectDashboardBuildTarget,
|
|
412
|
+
runBuild = runSqliteBuild,
|
|
413
|
+
onProgress = () => {},
|
|
414
|
+
} = {}) {
|
|
415
|
+
const builtTargets = [];
|
|
416
|
+
for (const [name, target] of Object.entries(targets)) {
|
|
417
|
+
buildProgress(
|
|
418
|
+
onProgress,
|
|
419
|
+
name === "frontend" ? 30 : 35,
|
|
420
|
+
`Verificando ${name === "frontend" ? "a interface" : "o runtime local"}`,
|
|
421
|
+
"Conferindo somente as dependências que geram este artefato.",
|
|
422
|
+
);
|
|
423
|
+
const state = await inspectTarget(target);
|
|
424
|
+
const needsBuild =
|
|
425
|
+
forceRebuild ||
|
|
426
|
+
state.missingArtifacts.length > 0 ||
|
|
427
|
+
state.artifactsModifiedAt < state.sourcesModifiedAt;
|
|
428
|
+
if (!needsBuild) {
|
|
429
|
+
buildProgress(
|
|
430
|
+
onProgress,
|
|
431
|
+
name === "frontend" ? 33 : 39,
|
|
432
|
+
`${name === "frontend" ? "Interface" : "Runtime local"} já está atualizado`,
|
|
433
|
+
"O artefato existente corresponde às dependências verificadas.",
|
|
434
|
+
);
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
const reason = forceRebuild
|
|
438
|
+
? "A atualização foi solicitada explicitamente."
|
|
439
|
+
: state.missingArtifacts.length > 0
|
|
440
|
+
? "Um artefato necessário está ausente."
|
|
441
|
+
: "Uma dependência deste artefato foi modificada.";
|
|
442
|
+
buildProgress(
|
|
443
|
+
onProgress,
|
|
444
|
+
name === "frontend" ? 33 : 39,
|
|
445
|
+
`Atualizando ${name === "frontend" ? "a interface" : "o runtime local"}`,
|
|
446
|
+
reason,
|
|
447
|
+
);
|
|
448
|
+
await runBuild(name);
|
|
449
|
+
builtTargets.push(name);
|
|
450
|
+
buildProgress(
|
|
451
|
+
onProgress,
|
|
452
|
+
name === "frontend" ? 35 : 42,
|
|
453
|
+
`${name === "frontend" ? "Interface" : "Runtime local"} atualizado`,
|
|
454
|
+
"O artefato foi gerado com as dependências atuais.",
|
|
455
|
+
);
|
|
333
456
|
}
|
|
457
|
+
return { artifacts: Object.values(targets).flatMap((target) => target.artifacts), builtTargets };
|
|
458
|
+
}
|
|
459
|
+
async function ensureFreshDashboardBuild(onProgress = () => {}) {
|
|
460
|
+
const { artifacts } = await refreshDashboardBuildTargets({ onProgress });
|
|
334
461
|
const artifactFingerprint = await Promise.all(
|
|
335
462
|
[...artifacts, resolve(packageRoot, "start-dashboard.mjs")].map(async (path) => {
|
|
336
463
|
const details = await stat(path);
|
|
@@ -481,6 +608,11 @@ export function startDashboardLaunchPage(port) {
|
|
|
481
608
|
return createDashboardLaunchPage({ port, host, runtimeKind, healthPath, delay });
|
|
482
609
|
}
|
|
483
610
|
async function redirectLaunchPage(launchPage, target) {
|
|
611
|
+
launchPage.setProgress({
|
|
612
|
+
percent: 100,
|
|
613
|
+
phase: "Dashboard pronto",
|
|
614
|
+
detail: "A tela principal está pronta para abrir.",
|
|
615
|
+
});
|
|
484
616
|
launchPage.redirect(target);
|
|
485
617
|
const redirected =
|
|
486
618
|
process.env.ZAIP_UI_NO_BROWSER === "1" ? true : await launchPage.waitForRedirect();
|
|
@@ -699,30 +831,13 @@ async function refreshSnapshotSeedInBackground() {
|
|
|
699
831
|
writeJsonAtomic,
|
|
700
832
|
});
|
|
701
833
|
}
|
|
702
|
-
async function writeLauncherStatus(status, message = "") {
|
|
703
|
-
const path = process.env.ZAIP_LAUNCH_STATUS;
|
|
704
|
-
if (!path) return;
|
|
705
|
-
const temporaryPath = `${path}.${process.pid}.tmp`;
|
|
706
|
-
await writeFile(temporaryPath, `${status}\n${message}`.trimEnd(), "utf8");
|
|
707
|
-
await rename(temporaryPath, path);
|
|
708
|
-
}
|
|
709
|
-
function phaseTiming(phase, startedAt, details = {}) {
|
|
710
|
-
appendLauncherLog(
|
|
711
|
-
JSON.stringify({
|
|
712
|
-
event: "dashboard_sqlite_phase",
|
|
713
|
-
phase,
|
|
714
|
-
elapsed_ms: Date.now() - startedAt,
|
|
715
|
-
launcher_pid: process.pid,
|
|
716
|
-
...details,
|
|
717
|
-
}),
|
|
718
|
-
);
|
|
719
|
-
}
|
|
720
834
|
async function main() {
|
|
721
835
|
let ownedChild = null;
|
|
722
836
|
let currentSession = null;
|
|
723
837
|
let launchPage = null;
|
|
724
838
|
const token = process.env.ZAIP_UI_TOKEN ?? randomBytes(24).toString("base64url");
|
|
725
839
|
const launchStartedAt = Date.now();
|
|
840
|
+
const phaseTiming = createPhaseTimer(appendLauncherLog, process.pid);
|
|
726
841
|
// Bind the preparation page atomically on an ephemeral port before waiting
|
|
727
842
|
// for the launch lock. Looking up a free port and binding it later lets a
|
|
728
843
|
// concurrent preparation page claim that port, which makes the runtime
|
|
@@ -755,7 +870,9 @@ async function main() {
|
|
|
755
870
|
phase: "Verificando os artefatos do dashboard",
|
|
756
871
|
detail: "Usando o build existente quando ele já está atualizado.",
|
|
757
872
|
});
|
|
758
|
-
const buildSignature = await ensureFreshDashboardBuild()
|
|
873
|
+
const buildSignature = await ensureFreshDashboardBuild((progress) =>
|
|
874
|
+
launchPage?.setProgress(progress),
|
|
875
|
+
);
|
|
759
876
|
phaseTiming("build_complete", buildStartedAt, { build_signature: buildSignature });
|
|
760
877
|
// A second deliberate click must use the already healthy compatible
|
|
761
878
|
// runtime. Build signature makes this safe across dashboard updates;
|
|
@@ -892,31 +1009,17 @@ async function main() {
|
|
|
892
1009
|
},
|
|
893
1010
|
);
|
|
894
1011
|
} catch (error) {
|
|
895
|
-
|
|
896
|
-
launchPage.setFailure({
|
|
897
|
-
percent: 100,
|
|
898
|
-
phase: "Não foi possível preparar o dashboard",
|
|
899
|
-
detail: formatLauncherError(error instanceof Error ? error.message : String(error), 900),
|
|
900
|
-
});
|
|
901
|
-
launchPage.closeAfterFailureObservation();
|
|
902
|
-
launchPage = null;
|
|
903
|
-
}
|
|
1012
|
+
launchPage = presentLaunchFailure(launchPage, error, formatLauncherError);
|
|
904
1013
|
throw error;
|
|
905
1014
|
}
|
|
906
1015
|
if (ownedChild === null) return;
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
1016
|
+
process.exitCode = await awaitOwnedDashboardExit({
|
|
1017
|
+
child: ownedChild,
|
|
1018
|
+
currentSession,
|
|
1019
|
+
clearOwnedDashboardSession,
|
|
1020
|
+
recordPortStatus,
|
|
1021
|
+
removeGeneratedReadModel,
|
|
913
1022
|
});
|
|
914
|
-
await clearOwnedDashboardSession();
|
|
915
|
-
if (currentSession !== null) {
|
|
916
|
-
await recordPortStatus(currentSession, "closed");
|
|
917
|
-
await removeGeneratedReadModel(currentSession.dbPath);
|
|
918
|
-
}
|
|
919
|
-
process.exitCode = exitCode;
|
|
920
1023
|
}
|
|
921
1024
|
function isPortFree(port) {
|
|
922
1025
|
return new Promise((resolvePort) => {
|
|
@@ -927,8 +1030,10 @@ function isPortFree(port) {
|
|
|
927
1030
|
});
|
|
928
1031
|
}
|
|
929
1032
|
async function waitForDashboard(port, child) {
|
|
930
|
-
|
|
931
|
-
|
|
1033
|
+
// A cold SQLite read model has no meaningful fixed duration: canonical
|
|
1034
|
+
// history and host disk speed both affect it. Keep request timeouts below,
|
|
1035
|
+
// but wait for an observable failure instead of a wall-clock cutoff.
|
|
1036
|
+
while (true) {
|
|
932
1037
|
if (child.exitCode !== null) {
|
|
933
1038
|
throw new Error(`Dashboard server exited before becoming healthy (${child.exitCode})`);
|
|
934
1039
|
}
|
|
@@ -962,11 +1067,6 @@ async function waitForDashboard(port, child) {
|
|
|
962
1067
|
}
|
|
963
1068
|
await delay(100);
|
|
964
1069
|
}
|
|
965
|
-
// A cold read-model timeout is not a port collision. Retrying would start
|
|
966
|
-
// up to four more full builds and exceed the launcher deadline.
|
|
967
|
-
const timedOut = new Error(`Timed out waiting for ${dashboardHealthUrl(port)}`);
|
|
968
|
-
timedOut.retryable = false;
|
|
969
|
-
throw timedOut;
|
|
970
1070
|
}
|
|
971
1071
|
function exists(path) {
|
|
972
1072
|
return access(path).then(
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { EventEmitter } from "node:events";
|
|
2
|
-
import { mkdir, mkdtemp, readFile, rm, utimes } from "node:fs/promises";
|
|
2
|
+
import { mkdir, mkdtemp, readFile, rm, utimes, writeFile } from "node:fs/promises";
|
|
3
3
|
import { createServer } from "node:net";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { join } from "node:path";
|
|
@@ -12,13 +12,16 @@ import {
|
|
|
12
12
|
findReusableDashboardSession,
|
|
13
13
|
formatLauncherError,
|
|
14
14
|
host,
|
|
15
|
+
inspectDashboardBuildTarget,
|
|
15
16
|
isAncestorProcess,
|
|
16
17
|
isProcessAlive,
|
|
18
|
+
latestModifiedAt,
|
|
17
19
|
openBrowser,
|
|
18
20
|
previousDashboardSessions,
|
|
19
21
|
probeDashboardPort,
|
|
20
22
|
readPortRegistry,
|
|
21
23
|
recordPortStatus,
|
|
24
|
+
refreshDashboardBuildTargets,
|
|
22
25
|
registeredListenerMatches,
|
|
23
26
|
runtimeKind,
|
|
24
27
|
stopOwnedDashboardProcess,
|
|
@@ -92,11 +95,143 @@ describe("dashboard executable session", () => {
|
|
|
92
95
|
expect(excludedPorts).toContain("launchPort,");
|
|
93
96
|
});
|
|
94
97
|
|
|
98
|
+
it("checks directed build inputs, including missing roots and every required artifact", async () => {
|
|
99
|
+
const directory = await mkdtemp(join(tmpdir(), "zaip-dashboard-build-inputs-"));
|
|
100
|
+
temporaryDirectories.push(directory);
|
|
101
|
+
const inputDirectory = join(directory, "input");
|
|
102
|
+
const artifactDirectory = join(directory, "artifacts");
|
|
103
|
+
const source = join(inputDirectory, "source.ts");
|
|
104
|
+
const testSource = join(inputDirectory, "source.test.ts");
|
|
105
|
+
const removedSource = join(inputDirectory, "removed-source.ts");
|
|
106
|
+
const excludedDirectory = join(inputDirectory, "excluded");
|
|
107
|
+
const excludedSource = join(excludedDirectory, "shared.ts");
|
|
108
|
+
const olderArtifact = join(artifactDirectory, "older-artifact.js");
|
|
109
|
+
const newerArtifact = join(artifactDirectory, "newer-artifact.js");
|
|
110
|
+
await Promise.all([mkdir(inputDirectory), mkdir(artifactDirectory)]);
|
|
111
|
+
await Promise.all([
|
|
112
|
+
writeFile(source, "source"),
|
|
113
|
+
writeFile(testSource, "test"),
|
|
114
|
+
writeFile(removedSource, "removed"),
|
|
115
|
+
mkdir(excludedDirectory),
|
|
116
|
+
writeFile(olderArtifact, "older"),
|
|
117
|
+
writeFile(newerArtifact, "newer"),
|
|
118
|
+
]);
|
|
119
|
+
await writeFile(excludedSource, "excluded");
|
|
120
|
+
const now = Date.now();
|
|
121
|
+
await Promise.all([
|
|
122
|
+
utimes(source, new Date(now), new Date(now)),
|
|
123
|
+
utimes(olderArtifact, new Date(now - 5_000), new Date(now - 5_000)),
|
|
124
|
+
utimes(newerArtifact, new Date(now + 5_000), new Date(now + 5_000)),
|
|
125
|
+
utimes(testSource, new Date(now + 10_000), new Date(now + 10_000)),
|
|
126
|
+
utimes(excludedSource, new Date(now + 10_000), new Date(now + 10_000)),
|
|
127
|
+
]);
|
|
128
|
+
|
|
129
|
+
await expect(latestModifiedAt([join(directory, "missing-root")])).resolves.toBe(0);
|
|
130
|
+
await expect(latestModifiedAt([inputDirectory], ["/excluded/"])).resolves.toBeLessThan(
|
|
131
|
+
now + 5_000,
|
|
132
|
+
);
|
|
133
|
+
await expect(
|
|
134
|
+
inspectDashboardBuildTarget({
|
|
135
|
+
artifacts: [olderArtifact, newerArtifact, join(directory, "missing-artifact.js")],
|
|
136
|
+
sourceRoots: [source, join(directory, "missing-root")],
|
|
137
|
+
}),
|
|
138
|
+
).resolves.toMatchObject({
|
|
139
|
+
artifactsModifiedAt: 0,
|
|
140
|
+
missingArtifacts: [join(directory, "missing-artifact.js")],
|
|
141
|
+
sourcesModifiedAt: expect.any(Number),
|
|
142
|
+
});
|
|
143
|
+
await expect(
|
|
144
|
+
inspectDashboardBuildTarget({
|
|
145
|
+
artifacts: [olderArtifact, newerArtifact],
|
|
146
|
+
sourceRoots: [source],
|
|
147
|
+
}),
|
|
148
|
+
).resolves.toMatchObject({
|
|
149
|
+
artifactsModifiedAt: expect.any(Number),
|
|
150
|
+
missingArtifacts: [],
|
|
151
|
+
sourcesModifiedAt: expect.any(Number),
|
|
152
|
+
});
|
|
153
|
+
const state = await inspectDashboardBuildTarget({
|
|
154
|
+
artifacts: [olderArtifact, newerArtifact],
|
|
155
|
+
sourceRoots: [source],
|
|
156
|
+
});
|
|
157
|
+
expect(state.artifactsModifiedAt).toBeLessThan(state.sourcesModifiedAt);
|
|
158
|
+
await rm(removedSource);
|
|
159
|
+
const stateAfterRemoval = await inspectDashboardBuildTarget({
|
|
160
|
+
artifacts: [olderArtifact, newerArtifact],
|
|
161
|
+
sourceRoots: [inputDirectory],
|
|
162
|
+
});
|
|
163
|
+
expect(stateAfterRemoval.artifactsModifiedAt).toBeLessThan(stateAfterRemoval.sourcesModifiedAt);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("keeps factual build milestones wired to the preparation page", async () => {
|
|
167
|
+
const source = await readFile(new URL("./start-dashboard.mjs", import.meta.url), "utf8");
|
|
168
|
+
|
|
169
|
+
expect(source).toContain("export const dashboardBuildTargets = {");
|
|
170
|
+
expect(source).toContain('resolve(workspaceRoot, "packages/core/src")');
|
|
171
|
+
expect(source).toContain('resolve(workspaceRoot, "packages/costs/src")');
|
|
172
|
+
expect(source).toContain('resolve(workspaceRoot, "packages/decisions/src")');
|
|
173
|
+
expect(source).toContain('resolve(workspaceRoot, "packages/lifecycle-manifests/src")');
|
|
174
|
+
expect(source).toContain('resolve(workspaceRoot, "packages/specs/src")');
|
|
175
|
+
expect(source).toContain('resolve(workspaceRoot, "packages/tasks/src")');
|
|
176
|
+
expect(source).toContain(
|
|
177
|
+
'resolve(workspaceRoot, ".agents/dashboard/packages/data-export/src")',
|
|
178
|
+
);
|
|
179
|
+
expect(source).toContain('resolve(workspaceRoot, ".agents/dashboard/packages/contracts/src")');
|
|
180
|
+
expect(source).toContain('resolve(workspaceRoot, "pnpm-lock.yaml")');
|
|
181
|
+
expect(source).toContain('resolve(packageRoot, "scripts")');
|
|
182
|
+
expect(source).toContain('excludedSourcePaths: ["/server/"]');
|
|
183
|
+
expect(source).toContain('excludedSourcePaths: ["/react/"]');
|
|
184
|
+
expect(source).toContain("if (/\\.(?:test|spec)\\.[cm]?[jt]sx?$/.test(name)) return false;");
|
|
185
|
+
expect(source).toContain("state.missingArtifacts.length > 0");
|
|
186
|
+
expect(source).toContain("Conferindo somente as dependências que geram este artefato.");
|
|
187
|
+
expect(source).toContain("launchPage?.setProgress(progress)");
|
|
188
|
+
expect(source).toContain("percent: 100,");
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("builds only the stale target and honors missing artifacts and an explicit force", async () => {
|
|
192
|
+
const targets = {
|
|
193
|
+
frontend: { artifacts: ["frontend-artifact"], sourceRoots: [] },
|
|
194
|
+
runtime: { artifacts: ["runtime-artifact"], sourceRoots: [] },
|
|
195
|
+
};
|
|
196
|
+
const fresh = { artifactsModifiedAt: 20, missingArtifacts: [], sourcesModifiedAt: 10 };
|
|
197
|
+
const stale = { artifactsModifiedAt: 10, missingArtifacts: [], sourcesModifiedAt: 20 };
|
|
198
|
+
const missing = {
|
|
199
|
+
artifactsModifiedAt: 0,
|
|
200
|
+
missingArtifacts: ["missing"],
|
|
201
|
+
sourcesModifiedAt: 10,
|
|
202
|
+
};
|
|
203
|
+
const verify = async (states, forceRebuild, expected) => {
|
|
204
|
+
const calls = [];
|
|
205
|
+
const progress = [];
|
|
206
|
+
const result = await refreshDashboardBuildTargets({
|
|
207
|
+
targets,
|
|
208
|
+
forceRebuild,
|
|
209
|
+
inspectTarget: async (target) => states[target.artifacts[0]],
|
|
210
|
+
runBuild: async (target) => calls.push(target),
|
|
211
|
+
onProgress: (event) => progress.push(event),
|
|
212
|
+
});
|
|
213
|
+
expect(calls).toEqual(expected);
|
|
214
|
+
expect(result.builtTargets).toEqual(expected);
|
|
215
|
+
expect(progress.every((event) => typeof event.phase === "string")).toBe(true);
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
await verify({ "frontend-artifact": fresh, "runtime-artifact": stale }, false, ["runtime"]);
|
|
219
|
+
await verify({ "frontend-artifact": stale, "runtime-artifact": fresh }, false, ["frontend"]);
|
|
220
|
+
await verify({ "frontend-artifact": fresh, "runtime-artifact": fresh }, false, []);
|
|
221
|
+
await verify({ "frontend-artifact": missing, "runtime-artifact": fresh }, false, ["frontend"]);
|
|
222
|
+
await verify({ "frontend-artifact": fresh, "runtime-artifact": fresh }, true, [
|
|
223
|
+
"frontend",
|
|
224
|
+
"runtime",
|
|
225
|
+
]);
|
|
226
|
+
});
|
|
227
|
+
|
|
95
228
|
it("opens preparation before checking freshness and keeps it through server health", async () => {
|
|
96
229
|
const source = await readFile(new URL("./start-dashboard.mjs", import.meta.url), "utf8");
|
|
97
230
|
const lock = source.indexOf("await withLauncherLock(");
|
|
98
231
|
const preparation = source.indexOf("launchPage = await startDashboardLaunchPage(0);");
|
|
99
|
-
const freshness = source.indexOf(
|
|
232
|
+
const freshness = source.indexOf(
|
|
233
|
+
"const buildSignature = await ensureFreshDashboardBuild((progress) =>",
|
|
234
|
+
);
|
|
100
235
|
const server = source.indexOf(
|
|
101
236
|
"const { child, port, dbPath, snapshotCache } = await startDashboardServer(",
|
|
102
237
|
);
|
|
@@ -109,7 +244,7 @@ describe("dashboard executable session", () => {
|
|
|
109
244
|
expect(redirectAfterServer).toBeGreaterThan(server);
|
|
110
245
|
});
|
|
111
246
|
|
|
112
|
-
it("reports readiness failures
|
|
247
|
+
it("reports real readiness failures without imposing a wall-clock limit on a cold rebuild", async () => {
|
|
113
248
|
const source = await readFile(new URL("./start-dashboard.mjs", import.meta.url), "utf8");
|
|
114
249
|
const wait = source.slice(
|
|
115
250
|
source.indexOf("async function waitForDashboard"),
|
|
@@ -117,7 +252,8 @@ describe("dashboard executable session", () => {
|
|
|
117
252
|
);
|
|
118
253
|
expect(wait).toContain("Dashboard data readiness returned ${readiness.status}");
|
|
119
254
|
expect(wait).toContain("unavailable.retryable = false;");
|
|
120
|
-
expect(wait).toContain("
|
|
255
|
+
expect(wait).toContain("while (true)");
|
|
256
|
+
expect(wait).not.toContain("startupTimeoutMs");
|
|
121
257
|
expect(wait).toContain("if (error?.retryable === false) throw error;");
|
|
122
258
|
expect(wait).toContain("await delay(100);");
|
|
123
259
|
});
|
|
@@ -611,7 +747,7 @@ describe("dashboard executable session", () => {
|
|
|
611
747
|
const preparation = source.indexOf("launchPage = await startDashboardLaunchPage(0);");
|
|
612
748
|
const lock = source.indexOf("await withLauncherLock(");
|
|
613
749
|
const buildSignature = source.indexOf(
|
|
614
|
-
"const buildSignature = await ensureFreshDashboardBuild()
|
|
750
|
+
"const buildSignature = await ensureFreshDashboardBuild((progress) =>",
|
|
615
751
|
);
|
|
616
752
|
const reusable = source.indexOf("const reusable = await findReusableDashboardSession({");
|
|
617
753
|
|
|
@@ -624,8 +760,13 @@ describe("dashboard executable session", () => {
|
|
|
624
760
|
expect(source).toContain(
|
|
625
761
|
'snapshotState: snapshotCache === "stale_seed" ? "updating" : "current",',
|
|
626
762
|
);
|
|
627
|
-
expect(source).toContain("launchPage
|
|
628
|
-
|
|
763
|
+
expect(source).toContain("presentLaunchFailure(launchPage, error, formatLauncherError)");
|
|
764
|
+
const lifecycleSource = await readFile(
|
|
765
|
+
new URL("./start-dashboard-lifecycle.mjs", import.meta.url),
|
|
766
|
+
"utf8",
|
|
767
|
+
);
|
|
768
|
+
expect(lifecycleSource).toContain("launchPage.setFailure({");
|
|
769
|
+
expect(lifecycleSource).toContain("launchPage.closeAfterFailureObservation();");
|
|
629
770
|
expect(source).toContain('await writeLauncherStatus("ok");');
|
|
630
771
|
});
|
|
631
772
|
|
|
@@ -640,15 +781,15 @@ describe("dashboard executable session", () => {
|
|
|
640
781
|
expect(beforeLock).not.toContain("findAvailablePortWithFallback(");
|
|
641
782
|
});
|
|
642
783
|
|
|
643
|
-
it("
|
|
784
|
+
it("keeps the Windows wrapper alive until Node reports success or a real failure", async () => {
|
|
644
785
|
const source = await readFile(
|
|
645
786
|
new URL("../windows-launcher/Program.cs", import.meta.url),
|
|
646
787
|
"utf8",
|
|
647
788
|
);
|
|
648
789
|
|
|
649
|
-
expect(source).toContain("
|
|
650
|
-
expect(source).toContain("
|
|
651
|
-
expect(source).toContain("
|
|
790
|
+
expect(source).toContain("while (true)");
|
|
791
|
+
expect(source).toContain("process.HasExited");
|
|
792
|
+
expect(source).not.toContain("AddSeconds(420)");
|
|
652
793
|
});
|
|
653
794
|
});
|
|
654
795
|
|