@jenga-ai/agent 3.1.1 → 3.4.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/README.md +52 -12
- package/agents/developer.md +31 -16
- package/agents/scrum-master.md +18 -17
- package/agents/tester.md +25 -15
- package/bin/jenga.js +10 -0
- package/lib/commands/dashboard.js +92 -0
- package/lib/skill-allow-list.json +7 -2
- package/package.json +21 -2
- package/project/app/api/lib/resolve-project-root.js +120 -0
- package/project/app/api/package.json +16 -0
- package/project/app/api/parsers/architecture.js +72 -0
- package/project/app/api/parsers/board.js +141 -0
- package/project/app/api/parsers/documentation.js +125 -0
- package/project/app/api/parsers/git-log.js +52 -0
- package/project/app/api/parsers/ideas.js +62 -0
- package/project/app/api/parsers/knowledge-graph.js +73 -0
- package/project/app/api/parsers/lib/markdown-dir-reader.js +163 -0
- package/project/app/api/parsers/rapports.js +148 -0
- package/project/app/api/parsers/todo.js +179 -0
- package/project/app/api/response.js +47 -0
- package/project/app/api/routes/architecture.js +23 -0
- package/project/app/api/routes/board.js +46 -0
- package/project/app/api/routes/documentation.js +24 -0
- package/project/app/api/routes/health.js +25 -0
- package/project/app/api/routes/history.js +55 -0
- package/project/app/api/routes/rapports.js +24 -0
- package/project/app/api/scripts/capture-snapshot.js +294 -0
- package/project/app/api/server.js +112 -0
- package/project/app/api/types.js +40 -0
- package/project/app/package.json +21 -0
- package/project/app/ui/dist/assets/index-7fj-vllY.js +104 -0
- package/project/app/ui/dist/assets/index-CdK3Qrep.css +1 -0
- package/project/app/ui/dist/index.html +13 -0
- package/project/app/ui/package.json +23 -0
- package/project/app/ui/scripts/build-snapshot-html.cjs +214 -0
- package/project/app/ui/scripts/dashboard-open.cjs +88 -0
- package/project/app/ui/scripts/dashboard-start.cjs +87 -0
- package/scripts/acquire-concurrency-slot.sh +220 -0
- package/scripts/audit-twin-divergence.sh +625 -0
- package/scripts/check-public-playbook-steps.sh +136 -0
- package/scripts/compute-deploy-reconcile.sh +439 -0
- package/scripts/jenga-permission-level-switch.sh +19 -3
- package/scripts/mark-deployed.sh +532 -0
- package/scripts/populate-knowledge-graph.js +429 -0
- package/scripts/release-concurrency-slot.sh +129 -0
- package/scripts/validate-board.sh +60 -2
- package/scripts/verify-consumer-install.sh +470 -0
- package/skills/j-close-story/SKILL.md +1 -1
- package/skills/j-cloud-connect/SKILL.md +95 -0
- package/skills/j-cloud-connect/scripts/configure-backend.sh +267 -0
- package/skills/j-cloud-connect/scripts/install-rclone.sh +153 -0
- package/skills/j-dashboard/SKILL.md +144 -0
- package/skills/j-dashboard/scripts/launch.sh +121 -0
- package/skills/j-dashboard/scripts/resolve-app-dir.sh +164 -0
- package/skills/j-dashboard/scripts/snapshot.sh +267 -0
- package/skills/j-dashboard-share/SKILL.md +96 -0
- package/skills/j-dashboard-share/scripts/upload-snapshot.sh +173 -0
- package/skills/j-do/SKILL.md +19 -19
- package/skills/j-doc-sync/SKILL.md +12 -1
- package/skills/j-idea/SKILL.md +1 -1
- package/skills/j-init/SKILL.md +5 -4
- package/skills/j-init/assets/directory_structure.txt +1 -0
- package/skills/j-init/scripts/detect-existing-codebase.sh +2 -2
- package/skills/j-init/scripts/init.sh +13 -2
- package/skills/j-playbook/SKILL.md +93 -0
- package/skills/j-playbook-new/SKILL.md +155 -0
- package/skills/j-playbook-new/scripts/playbook-new.sh +332 -0
- package/skills/j-proceed/SKILL.md +1 -1
- package/skills/j-publish/SKILL.md +1 -1
- package/skills/j-publish/adapters/npm-ci.md +29 -0
- package/skills/j-publish/scripts/npm_ci_pipeline.sh +9 -0
- package/skills/j-publish/scripts/npm_pipeline.sh +18 -0
- package/skills/j-publish/scripts/npm_stage_pipeline.sh +81 -41
- package/skills/j-reconcile/SKILL.md +1 -0
- package/skills/j-redo/SKILL.md +1 -1
- package/skills/j-status/SKILL.md +12 -0
- package/skills/j-todo/SKILL.md +2 -2
- package/skills/j-uncharted/SKILL.md +8 -7
- package/skills/j-uncharted/scripts/validate-proposed-items.sh +18 -2
- package/skills/jenga/SKILL.md +55 -16
- package/skills/jenga/playbooks/idea-to-committed.json +20 -0
- package/skills/jenga/playbooks/schema.json +1 -1
- package/skills/jenga/scripts/load-nl-catalog.js +22 -6
- package/skills/jenga/scripts/load-playbooks.sh +968 -41
- package/skills/jenga/scripts/match-playbook.sh +1 -1
- package/skills/jenga/scripts/render-playbook-confirmation.sh +162 -8
- package/skills/jenga/scripts/run-playbook-step.sh +535 -42
- package/skills/jenga-permission-level/SKILL.md +4 -4
- package/templates/KNOWLEDGE_GRAPH_STUB_SCHEMA_TEMPLATE.md +128 -0
- package/templates/SCRUM_BOARD_SCHEMA.md +18 -6
- package/templates/playbook-types.json +8 -0
- package/skills/jenga/playbooks/brainstorm-to-mirror.json +0 -22
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file api/response.js
|
|
3
|
+
* Helper functions for building consistent API response envelopes.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const API_VERSION = '1.0.0';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Build a success envelope.
|
|
10
|
+
* @template T
|
|
11
|
+
* @param {T} data
|
|
12
|
+
* @param {Object} [extraMeta] - Additional meta fields to merge
|
|
13
|
+
* @returns {import('./types').ApiEnvelope<T>}
|
|
14
|
+
*/
|
|
15
|
+
function successResponse(data, extraMeta = {}) {
|
|
16
|
+
return {
|
|
17
|
+
data,
|
|
18
|
+
meta: {
|
|
19
|
+
timestamp: new Date().toISOString(),
|
|
20
|
+
version: API_VERSION,
|
|
21
|
+
...extraMeta,
|
|
22
|
+
},
|
|
23
|
+
error: null,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Build an error envelope.
|
|
29
|
+
* @param {string} code - Error code from ERROR_CODES
|
|
30
|
+
* @param {string} message - Human-readable message
|
|
31
|
+
* @param {Object} [details]
|
|
32
|
+
* @returns {import('./types').ApiEnvelope<null>}
|
|
33
|
+
*/
|
|
34
|
+
function errorResponse(code, message, details) {
|
|
35
|
+
const err = { code, message };
|
|
36
|
+
if (details !== undefined) err.details = details;
|
|
37
|
+
return {
|
|
38
|
+
data: null,
|
|
39
|
+
meta: {
|
|
40
|
+
timestamp: new Date().toISOString(),
|
|
41
|
+
version: API_VERSION,
|
|
42
|
+
},
|
|
43
|
+
error: err,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
module.exports = { successResponse, errorResponse, API_VERSION };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file project/app/api/routes/architecture.js
|
|
3
|
+
* GET /architecture — tech stack and dependency metadata
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { Router } = require('express');
|
|
7
|
+
const { parseArchitecture } = require('../parsers/architecture');
|
|
8
|
+
const { successResponse, errorResponse } = require('../response');
|
|
9
|
+
const { ERROR_CODES } = require('../types');
|
|
10
|
+
|
|
11
|
+
const router = Router();
|
|
12
|
+
|
|
13
|
+
router.get('/', async (req, res) => {
|
|
14
|
+
try {
|
|
15
|
+
const arch = await parseArchitecture();
|
|
16
|
+
res.json(successResponse(arch));
|
|
17
|
+
} catch (err) {
|
|
18
|
+
console.error('[architecture] error:', err.message);
|
|
19
|
+
res.status(500).json(errorResponse(ERROR_CODES.INTERNAL_ERROR, err.message));
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
module.exports = router;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file project/app/api/routes/board.js
|
|
3
|
+
* GET /board — full nested board
|
|
4
|
+
* GET /board/:epicId — single epic (case-insensitive)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { Router } = require('express');
|
|
8
|
+
const { parseBoard } = require('../parsers/board');
|
|
9
|
+
const { successResponse, errorResponse } = require('../response');
|
|
10
|
+
const { ERROR_CODES } = require('../types');
|
|
11
|
+
|
|
12
|
+
const router = Router();
|
|
13
|
+
|
|
14
|
+
router.get('/', async (req, res) => {
|
|
15
|
+
try {
|
|
16
|
+
const board = await parseBoard();
|
|
17
|
+
res.json(successResponse(board));
|
|
18
|
+
} catch (err) {
|
|
19
|
+
console.error('[board] parse error:', err.message);
|
|
20
|
+
res.status(500).json(errorResponse(ERROR_CODES.PARSE_ERROR, err.message));
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
router.get('/:epicId', async (req, res) => {
|
|
25
|
+
try {
|
|
26
|
+
const board = await parseBoard();
|
|
27
|
+
const id = req.params.epicId.toLowerCase();
|
|
28
|
+
const epic = board.find((e) => e.id && e.id.toLowerCase() === id);
|
|
29
|
+
if (!epic) {
|
|
30
|
+
return res
|
|
31
|
+
.status(404)
|
|
32
|
+
.json(
|
|
33
|
+
errorResponse(
|
|
34
|
+
ERROR_CODES.EPIC_NOT_FOUND,
|
|
35
|
+
`Epic '${req.params.epicId}' not found`
|
|
36
|
+
)
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
res.json(successResponse(epic));
|
|
40
|
+
} catch (err) {
|
|
41
|
+
console.error('[board/:epicId] parse error:', err.message);
|
|
42
|
+
res.status(500).json(errorResponse(ERROR_CODES.PARSE_ERROR, err.message));
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
module.exports = router;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file project/app/api/routes/documentation.js
|
|
3
|
+
* GET / — full documentation aggregate (summary/readme/strategy/example), untruncated, no query
|
|
4
|
+
* params or pagination — matches GET /v1/board's bulk-fetch precedent.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { Router } = require('express');
|
|
8
|
+
const { readDocumentation } = require('../parsers/documentation');
|
|
9
|
+
const { successResponse, errorResponse } = require('../response');
|
|
10
|
+
const { ERROR_CODES } = require('../types');
|
|
11
|
+
|
|
12
|
+
const router = Router();
|
|
13
|
+
|
|
14
|
+
router.get('/', async (req, res) => {
|
|
15
|
+
try {
|
|
16
|
+
const documentation = await readDocumentation();
|
|
17
|
+
res.json(successResponse(documentation));
|
|
18
|
+
} catch (err) {
|
|
19
|
+
console.error('[documentation] parse error:', err.message);
|
|
20
|
+
res.status(500).json(errorResponse(ERROR_CODES.PARSE_ERROR, err.message));
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
module.exports = router;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file project/app/api/routes/health.js
|
|
3
|
+
* GET /health — server liveness check
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { Router } = require('express');
|
|
7
|
+
const { successResponse } = require('../response');
|
|
8
|
+
const { API_VERSION } = require('../response');
|
|
9
|
+
|
|
10
|
+
const router = Router();
|
|
11
|
+
|
|
12
|
+
const startTime = Date.now();
|
|
13
|
+
|
|
14
|
+
router.get('/', (req, res) => {
|
|
15
|
+
const uptime = (Date.now() - startTime) / 1000;
|
|
16
|
+
res.json(
|
|
17
|
+
successResponse({
|
|
18
|
+
status: 'ok',
|
|
19
|
+
version: API_VERSION,
|
|
20
|
+
uptime,
|
|
21
|
+
})
|
|
22
|
+
);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
module.exports = router;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file project/app/api/routes/history.js
|
|
3
|
+
* GET /history — merged git commits + rapport files, sorted by date desc
|
|
4
|
+
* Query params:
|
|
5
|
+
* ?limit=N — return first N items
|
|
6
|
+
* ?type=git_commit|rapport — filter by type
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const { Router } = require('express');
|
|
10
|
+
const { readGitLog } = require('../parsers/git-log');
|
|
11
|
+
const { readRapports } = require('../parsers/rapports');
|
|
12
|
+
const { successResponse, errorResponse } = require('../response');
|
|
13
|
+
const { ERROR_CODES } = require('../types');
|
|
14
|
+
|
|
15
|
+
const router = Router();
|
|
16
|
+
|
|
17
|
+
router.get('/', async (req, res) => {
|
|
18
|
+
const { limit, type } = req.query;
|
|
19
|
+
|
|
20
|
+
if (limit !== undefined && (isNaN(Number(limit)) || Number(limit) < 1)) {
|
|
21
|
+
return res
|
|
22
|
+
.status(400)
|
|
23
|
+
.json(
|
|
24
|
+
errorResponse(ERROR_CODES.INVALID_QUERY_PARAM, '`limit` must be a positive integer')
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
if (type !== undefined && !['git_commit', 'rapport'].includes(type)) {
|
|
28
|
+
return res
|
|
29
|
+
.status(400)
|
|
30
|
+
.json(
|
|
31
|
+
errorResponse(ERROR_CODES.INVALID_QUERY_PARAM, '`type` must be git_commit or rapport')
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const [commits, rapports] = await Promise.all([readGitLog(), readRapports()]);
|
|
37
|
+
let items = [...commits, ...rapports];
|
|
38
|
+
|
|
39
|
+
items.sort((a, b) => {
|
|
40
|
+
const da = new Date(a.date || 0).getTime();
|
|
41
|
+
const db = new Date(b.date || 0).getTime();
|
|
42
|
+
return db - da;
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
if (type) items = items.filter((i) => i.type === type);
|
|
46
|
+
if (limit) items = items.slice(0, Number(limit));
|
|
47
|
+
|
|
48
|
+
res.json(successResponse(items));
|
|
49
|
+
} catch (err) {
|
|
50
|
+
console.error('[history] error:', err.message);
|
|
51
|
+
res.status(500).json(errorResponse(ERROR_CODES.INTERNAL_ERROR, err.message));
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
module.exports = router;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file project/app/api/routes/rapports.js
|
|
3
|
+
* GET / — full rapports aggregate (analysis/problems/tests/summaries/plans/idea), untruncated,
|
|
4
|
+
* no query params or pagination — matches GET /v1/board's bulk-fetch precedent.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { Router } = require('express');
|
|
8
|
+
const { readRapportsFull } = require('../parsers/rapports');
|
|
9
|
+
const { successResponse, errorResponse } = require('../response');
|
|
10
|
+
const { ERROR_CODES } = require('../types');
|
|
11
|
+
|
|
12
|
+
const router = Router();
|
|
13
|
+
|
|
14
|
+
router.get('/', async (req, res) => {
|
|
15
|
+
try {
|
|
16
|
+
const rapports = await readRapportsFull();
|
|
17
|
+
res.json(successResponse(rapports));
|
|
18
|
+
} catch (err) {
|
|
19
|
+
console.error('[rapports] parse error:', err.message);
|
|
20
|
+
res.status(500).json(errorResponse(ERROR_CODES.PARSE_ERROR, err.message));
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
module.exports = router;
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @file project/app/api/scripts/capture-snapshot.js
|
|
4
|
+
*
|
|
5
|
+
* E47_S04_T02 — capture step for `j.dashboard --snapshot`.
|
|
6
|
+
*
|
|
7
|
+
* Calls the dashboard API's `/v1/board`, `/v1/history`, and `/v1/architecture` routes exactly once
|
|
8
|
+
* each and writes their combined JSON to a single artifact, consumable by the bundling/inlining
|
|
9
|
+
* step in `E47_S04_T03` (not yet implemented — this script's job ends at "artifact written to
|
|
10
|
+
* disk").
|
|
11
|
+
*
|
|
12
|
+
* ── Why an ad-hoc, in-process server instead of hitting an already-running one ──────────────────
|
|
13
|
+
* This script always spins up its own short-lived, ephemeral (`--port 0` by default) server
|
|
14
|
+
* instance bound to an explicitly resolved project root, rather than assuming or reaching for some
|
|
15
|
+
* already-running dashboard server. An already-running server on a well-known port (e.g. 3001)
|
|
16
|
+
* could be serving an entirely different project root's data — reusing it would silently defeat
|
|
17
|
+
* the "capture the *invoking* project's data, not this repo's" requirement this task exists to
|
|
18
|
+
* satisfy. A freshly spawned, request-scoped instance removes that ambiguity entirely: whatever
|
|
19
|
+
* root this script resolves is unambiguously the root the three routes below will answer for.
|
|
20
|
+
*
|
|
21
|
+
* ── Reuses E47_S02's generalized data-source resolution — does not re-derive it ─────────────────
|
|
22
|
+
* `../lib/resolve-project-root.js`'s `resolveProjectRoot()` (added by E47_S02_T01, wired into
|
|
23
|
+
* `../server.js` and every parser by E47_S02_T02) is the sole path-resolution mechanism used here:
|
|
24
|
+
* - `--project-root <path>`, if given, is set as the `JENGA_PROJECT_ROOT` env var *before*
|
|
25
|
+
* calling `resolveProjectRoot()` — i.e. it drives E47_S02's own primary override mechanism
|
|
26
|
+
* (the same one `server.js`'s own doc comment names as the future `jenga dashboard start` CLI's
|
|
27
|
+
* intended integration path), rather than inventing a second override convention.
|
|
28
|
+
* - If `--project-root` is not given, `resolveProjectRoot({ cwd: process.cwd() })` is called with
|
|
29
|
+
* no override — exactly mirroring `server.js`'s own no-override behavior (walk up from cwd
|
|
30
|
+
* looking for a `project/board` marker, fail loudly if none is found within the bound).
|
|
31
|
+
* - Either way, the *resolved* (realpath'd) value is then re-pinned onto `JENGA_PROJECT_ROOT`
|
|
32
|
+
* before `../server` is required, so `server.js`'s own load-time check
|
|
33
|
+
* (`if (!process.env.JENGA_PROJECT_ROOT) { ... }`) short-circuits and never re-derives
|
|
34
|
+
* resolution — every parser downstream agrees with exactly what this script already validated.
|
|
35
|
+
* If `resolveProjectRoot()` ever throws (e.g. no `project/board` marker found, or an explicit
|
|
36
|
+
* `--project-root` that doesn't exist), this script fails loudly with that same descriptive error
|
|
37
|
+
* and never starts a server or writes a partial artifact.
|
|
38
|
+
*
|
|
39
|
+
* ── Output artifact shape (the hand-off contract for E47_S04_T03) ────────────────────────────────
|
|
40
|
+
* A single UTF-8 JSON file:
|
|
41
|
+
* {
|
|
42
|
+
* "schema_version": 1,
|
|
43
|
+
* "captured_at": "<ISO 8601 UTC timestamp>",
|
|
44
|
+
* "project_root": "<absolute, realpath'd project root this snapshot was captured against>",
|
|
45
|
+
* "routes": {
|
|
46
|
+
* "board": <full API envelope from GET /v1/board, i.e. { data, meta, error }>,
|
|
47
|
+
* "history": <full API envelope from GET /v1/history, i.e. { data, meta, error }>,
|
|
48
|
+
* "architecture": <full API envelope from GET /v1/architecture, i.e. { data, meta, error }>
|
|
49
|
+
* }
|
|
50
|
+
* }
|
|
51
|
+
* Each `routes.<name>` value is the *full* envelope exactly as the route returned it (see
|
|
52
|
+
* `../response.js`'s `successResponse`/`errorResponse` shape) — deliberately not unwrapped to a
|
|
53
|
+
* bare `.data` — so `E47_S04_T03`'s bundler can decide for itself how to feed this into the UI's
|
|
54
|
+
* existing `client.js` request contract (which itself expects `{ data, meta, error }` and throws on
|
|
55
|
+
* a truthy `error`) without this task guessing that choice on its behalf. Since capture already
|
|
56
|
+
* hard-fails on any route error (see below), `error` will always be `null` in a written artifact —
|
|
57
|
+
* it is kept in the shape anyway so the two representations (a live API response vs. a captured
|
|
58
|
+
* one) stay structurally identical.
|
|
59
|
+
*
|
|
60
|
+
* ── Failure behavior ──────────────────────────────────────────────────────────────────────────
|
|
61
|
+
* Any of the following is treated as a hard failure: the ad-hoc server fails to start, a route
|
|
62
|
+
* request errors at the network level, a route responds with a non-2xx status, a route responds
|
|
63
|
+
* with non-JSON, or a route's own JSON envelope carries a truthy `error` field. In every case: the
|
|
64
|
+
* ad-hoc server is closed, a specific, actionable message is printed to stderr (naming the route
|
|
65
|
+
* and the failure reason), the process exits non-zero, and **no artifact is written** — never a
|
|
66
|
+
* partial or silently-empty snapshot.
|
|
67
|
+
*
|
|
68
|
+
* ── Node compatibility ────────────────────────────────────────────────────────────────────────
|
|
69
|
+
* Uses the built-in `http` module rather than global `fetch`, to stay compatible with the root
|
|
70
|
+
* `package.json`'s declared `engines.node: >=14.13.1` floor (global `fetch` only became available
|
|
71
|
+
* unflagged in Node 18).
|
|
72
|
+
*
|
|
73
|
+
* Usage:
|
|
74
|
+
* node capture-snapshot.js [--project-root <path>] [--out <path>] [--port <n>]
|
|
75
|
+
*
|
|
76
|
+
* --project-root <path> Explicit project root override (see above). Default: resolve by
|
|
77
|
+
* walking up from the current working directory.
|
|
78
|
+
* --out <path> Where to write the combined snapshot JSON. Default:
|
|
79
|
+
* "./dashboard-snapshot-data.json" (relative to cwd). Final on-disk
|
|
80
|
+
* location for a real `--snapshot` run is E47_S04_T03's call, not this
|
|
81
|
+
* script's — it only writes wherever `--out` points.
|
|
82
|
+
* --port <n> Port for the ad-hoc capture server. Default: 0 (OS-assigned ephemeral
|
|
83
|
+
* port) — deliberately not the dashboard's usual default port, so this
|
|
84
|
+
* never collides with (or is confused for) a real already-running server.
|
|
85
|
+
*/
|
|
86
|
+
|
|
87
|
+
'use strict';
|
|
88
|
+
|
|
89
|
+
const path = require('path');
|
|
90
|
+
const http = require('http');
|
|
91
|
+
const fs = require('fs');
|
|
92
|
+
|
|
93
|
+
const ROUTES = ['/v1/board', '/v1/history', '/v1/architecture'];
|
|
94
|
+
const ROUTE_KEYS = {
|
|
95
|
+
'/v1/board': 'board',
|
|
96
|
+
'/v1/history': 'history',
|
|
97
|
+
'/v1/architecture': 'architecture',
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
function printUsage() {
|
|
101
|
+
console.log(`Usage: capture-snapshot.js [--project-root <path>] [--out <path>] [--port <n>]
|
|
102
|
+
|
|
103
|
+
--project-root <path> Explicit project root override, passed through to E47_S02's
|
|
104
|
+
resolveProjectRoot() as the JENGA_PROJECT_ROOT override (the same
|
|
105
|
+
mechanism server.js itself honors). Defaults to walking up from the
|
|
106
|
+
current working directory looking for a project/board marker.
|
|
107
|
+
--out <path> Where to write the combined snapshot JSON.
|
|
108
|
+
Default: ./dashboard-snapshot-data.json (relative to cwd).
|
|
109
|
+
--port <n> Port for the ad-hoc capture server instance. Default: 0
|
|
110
|
+
(OS-assigned ephemeral port).
|
|
111
|
+
`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function die(message) {
|
|
115
|
+
console.error(`Error: capture-snapshot: ${message}`);
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function parseArgs(argv) {
|
|
120
|
+
const args = { projectRoot: null, out: null, port: 0 };
|
|
121
|
+
for (let i = 0; i < argv.length; i++) {
|
|
122
|
+
const token = argv[i];
|
|
123
|
+
switch (token) {
|
|
124
|
+
case '--project-root':
|
|
125
|
+
args.projectRoot = argv[++i];
|
|
126
|
+
if (!args.projectRoot) die('--project-root requires a value');
|
|
127
|
+
break;
|
|
128
|
+
case '--out':
|
|
129
|
+
args.out = argv[++i];
|
|
130
|
+
if (!args.out) die('--out requires a value');
|
|
131
|
+
break;
|
|
132
|
+
case '--port': {
|
|
133
|
+
const raw = argv[++i];
|
|
134
|
+
const parsed = parseInt(raw, 10);
|
|
135
|
+
if (raw === undefined || isNaN(parsed) || parsed < 0 || parsed > 65535) {
|
|
136
|
+
die(`--port must be a valid port number (0-65535), got: ${raw}`);
|
|
137
|
+
}
|
|
138
|
+
args.port = parsed;
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
case '-h':
|
|
142
|
+
case '--help':
|
|
143
|
+
printUsage();
|
|
144
|
+
process.exit(0);
|
|
145
|
+
break;
|
|
146
|
+
default:
|
|
147
|
+
printUsage();
|
|
148
|
+
die(`unknown argument: ${token}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return args;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Issue a single GET request to the ad-hoc capture server and parse its JSON envelope. Rejects
|
|
156
|
+
* (rather than resolving with a partial/error payload) on any network error, non-2xx status,
|
|
157
|
+
* non-JSON body, or a truthy `error` field in the parsed envelope — the caller treats a rejection
|
|
158
|
+
* as a hard capture failure.
|
|
159
|
+
*/
|
|
160
|
+
function fetchJson(port, routePath) {
|
|
161
|
+
return new Promise((resolve, reject) => {
|
|
162
|
+
const req = http.get(
|
|
163
|
+
{ host: '127.0.0.1', port, path: routePath, timeout: 15000 },
|
|
164
|
+
(res) => {
|
|
165
|
+
let body = '';
|
|
166
|
+
res.setEncoding('utf8');
|
|
167
|
+
res.on('data', (chunk) => {
|
|
168
|
+
body += chunk;
|
|
169
|
+
});
|
|
170
|
+
res.on('end', () => {
|
|
171
|
+
let parsed;
|
|
172
|
+
try {
|
|
173
|
+
parsed = JSON.parse(body);
|
|
174
|
+
} catch (err) {
|
|
175
|
+
reject(
|
|
176
|
+
new Error(
|
|
177
|
+
`${routePath} returned a non-JSON response (status ${res.statusCode}): ${err.message}`
|
|
178
|
+
)
|
|
179
|
+
);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (res.statusCode < 200 || res.statusCode >= 300 || (parsed && parsed.error)) {
|
|
183
|
+
const reason =
|
|
184
|
+
parsed && parsed.error
|
|
185
|
+
? `${parsed.error.code}: ${parsed.error.message}`
|
|
186
|
+
: `HTTP ${res.statusCode}`;
|
|
187
|
+
reject(new Error(`${routePath} returned an error response — ${reason}`));
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
resolve(parsed);
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
);
|
|
194
|
+
req.on('timeout', () => {
|
|
195
|
+
req.destroy(new Error(`${routePath} timed out after 15s — is the capture server reachable?`));
|
|
196
|
+
});
|
|
197
|
+
req.on('error', (err) => {
|
|
198
|
+
reject(new Error(`${routePath} request failed: ${err.message} — is the capture server reachable?`));
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function startServer(app, port) {
|
|
204
|
+
return new Promise((resolve, reject) => {
|
|
205
|
+
const server = app.listen(port, '127.0.0.1');
|
|
206
|
+
server.once('listening', () => resolve(server));
|
|
207
|
+
server.once('error', (err) => {
|
|
208
|
+
reject(new Error(`could not start the ad-hoc capture server on port ${port}: ${err.message}`));
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async function main() {
|
|
214
|
+
const args = parseArgs(process.argv.slice(2));
|
|
215
|
+
|
|
216
|
+
// eslint-disable-next-line global-require
|
|
217
|
+
const { resolveProjectRoot } = require('../lib/resolve-project-root');
|
|
218
|
+
|
|
219
|
+
if (args.projectRoot) {
|
|
220
|
+
// Drive E47_S02's own primary override mechanism rather than inventing a second one.
|
|
221
|
+
process.env.JENGA_PROJECT_ROOT = args.projectRoot;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
let resolvedRoot;
|
|
225
|
+
try {
|
|
226
|
+
resolvedRoot = resolveProjectRoot();
|
|
227
|
+
} catch (err) {
|
|
228
|
+
die(err.message);
|
|
229
|
+
return; // unreachable — die() exits — but keeps linters happy about resolvedRoot's usage below
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Re-pin the *resolved* (realpath'd) value so server.js's own load-time resolution becomes a
|
|
233
|
+
// no-op and every downstream parser agrees with what was just validated here.
|
|
234
|
+
process.env.JENGA_PROJECT_ROOT = resolvedRoot;
|
|
235
|
+
|
|
236
|
+
const outPath = path.resolve(args.out || 'dashboard-snapshot-data.json');
|
|
237
|
+
|
|
238
|
+
let app;
|
|
239
|
+
try {
|
|
240
|
+
// eslint-disable-next-line global-require
|
|
241
|
+
({ app } = require('../server'));
|
|
242
|
+
} catch (err) {
|
|
243
|
+
die(`failed to load the dashboard API server: ${err.message}`);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
let server;
|
|
248
|
+
try {
|
|
249
|
+
server = await startServer(app, args.port);
|
|
250
|
+
} catch (err) {
|
|
251
|
+
die(err.message);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const boundPort = server.address().port;
|
|
256
|
+
|
|
257
|
+
const routeResults = {};
|
|
258
|
+
try {
|
|
259
|
+
for (const routePath of ROUTES) {
|
|
260
|
+
// Sequential, not parallel: on the first failure we stop immediately with a clear,
|
|
261
|
+
// single-cause error rather than a pile of concurrent rejections to untangle.
|
|
262
|
+
// eslint-disable-next-line no-await-in-loop
|
|
263
|
+
routeResults[ROUTE_KEYS[routePath]] = await fetchJson(boundPort, routePath);
|
|
264
|
+
}
|
|
265
|
+
} catch (err) {
|
|
266
|
+
server.close();
|
|
267
|
+
die(`snapshot capture failed — ${err.message}. No artifact was written.`);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
server.close();
|
|
272
|
+
|
|
273
|
+
const snapshot = {
|
|
274
|
+
schema_version: 1,
|
|
275
|
+
captured_at: new Date().toISOString(),
|
|
276
|
+
project_root: resolvedRoot,
|
|
277
|
+
routes: routeResults,
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
try {
|
|
281
|
+
fs.writeFileSync(outPath, `${JSON.stringify(snapshot, null, 2)}\n`);
|
|
282
|
+
} catch (err) {
|
|
283
|
+
die(`failed to write snapshot artifact to ${outPath}: ${err.message}`);
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
console.log(`Snapshot captured: ${outPath}`);
|
|
288
|
+
console.log(` project_root: ${resolvedRoot}`);
|
|
289
|
+
console.log(` routes: ${Object.keys(routeResults).join(', ')}`);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
main().catch((err) => {
|
|
293
|
+
die(err && err.stack ? err.stack : String(err));
|
|
294
|
+
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file project/app/api/server.js
|
|
3
|
+
* Jenga AI API server entry point.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* node project/app/api/server.js
|
|
7
|
+
* JENGA_API_PORT=4000 node project/app/api/server.js
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
const { resolveProjectRoot } = require('./lib/resolve-project-root');
|
|
13
|
+
|
|
14
|
+
// Pin the resolved project root as an explicit override *before* any router/parser module loads —
|
|
15
|
+
// each parser (board.js, git-log.js, rapports.js, architecture.js, knowledge-graph.js) computes its
|
|
16
|
+
// own root via resolveProjectRoot() at its own module-load time, so this must run first.
|
|
17
|
+
//
|
|
18
|
+
// Deliberately calls resolveProjectRoot() rather than setting a raw `process.cwd()` literal: the
|
|
19
|
+
// real launch paths in this repo (root `npm run dashboard:start`/`api:start`, both of which use
|
|
20
|
+
// `--prefix project/app`) run with `cwd` set to `project/app` by npm, not the repo root — and
|
|
21
|
+
// `project/app` itself has no `project/board` two levels below it (`project/board` is a *sibling*
|
|
22
|
+
// of `project/app`). A raw `cwd` override would incorrectly `throw` in that case. Calling
|
|
23
|
+
// resolveProjectRoot() here performs the same override-else-walk-up resolution the module already
|
|
24
|
+
// does (walk-up from `cwd` correctly finds `project/board` above `project/app`), then pins that
|
|
25
|
+
// *resolved* value into the env var — every parser's own subsequent call becomes a cheap env-var
|
|
26
|
+
// read instead of repeating the walk, and every parser is guaranteed to agree on the same root even
|
|
27
|
+
// if `process.cwd()` changes later in this process's lifetime. If a caller already set an override
|
|
28
|
+
// (e.g. a future `jenga dashboard start` CLI subcommand that knows the user's true invocation
|
|
29
|
+
// directory more reliably than this process's own `cwd`), this is a no-op — resolveProjectRoot()
|
|
30
|
+
// already treats an explicit override as authoritative over any walk-up.
|
|
31
|
+
if (!process.env.JENGA_PROJECT_ROOT) {
|
|
32
|
+
process.env.JENGA_PROJECT_ROOT = resolveProjectRoot();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const express = require('express');
|
|
36
|
+
const cors = require('cors');
|
|
37
|
+
|
|
38
|
+
const healthRouter = require('./routes/health');
|
|
39
|
+
const boardRouter = require('./routes/board');
|
|
40
|
+
const historyRouter = require('./routes/history');
|
|
41
|
+
const architectureRouter = require('./routes/architecture');
|
|
42
|
+
const rapportsRouter = require('./routes/rapports');
|
|
43
|
+
const documentationRouter = require('./routes/documentation');
|
|
44
|
+
|
|
45
|
+
const { API_VERSION } = require('./response');
|
|
46
|
+
|
|
47
|
+
const PORT = parseInt(process.env.JENGA_API_PORT || '3001', 10);
|
|
48
|
+
|
|
49
|
+
const app = express();
|
|
50
|
+
|
|
51
|
+
// ── Middleware ─────────────────────────────────────────────────────────────────
|
|
52
|
+
app.use(cors());
|
|
53
|
+
app.use(express.json());
|
|
54
|
+
|
|
55
|
+
// ── Routes ─────────────────────────────────────────────────────────────────────
|
|
56
|
+
app.use('/v1/health', healthRouter);
|
|
57
|
+
app.use('/v1/board', boardRouter);
|
|
58
|
+
app.use('/v1/history', historyRouter);
|
|
59
|
+
app.use('/v1/architecture', architectureRouter);
|
|
60
|
+
app.use('/v1/rapports', rapportsRouter);
|
|
61
|
+
app.use('/v1/documentation', documentationRouter);
|
|
62
|
+
|
|
63
|
+
// ── Fallback routes ────────────────────────────────────────────────────────────
|
|
64
|
+
// Registered on demand (not at require-time) so callers that mount additional
|
|
65
|
+
// routes on `app` after requiring this module — e.g. dashboard-start.cjs
|
|
66
|
+
// serving the built UI — can do so before the catch-all 404 swallows them.
|
|
67
|
+
const { errorResponse } = require('./response');
|
|
68
|
+
const { ERROR_CODES } = require('./types');
|
|
69
|
+
|
|
70
|
+
function attachFallbackRoutes(targetApp, { rootRedirect = true } = {}) {
|
|
71
|
+
if (rootRedirect) {
|
|
72
|
+
targetApp.get('/', (req, res) => res.redirect('/v1/health'));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
targetApp.use((req, res) => {
|
|
76
|
+
res.status(404).json(errorResponse(ERROR_CODES.NOT_FOUND, `Route '${req.path}' not found`));
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ── Start ──────────────────────────────────────────────────────────────────────
|
|
81
|
+
let server;
|
|
82
|
+
|
|
83
|
+
if (require.main === module) {
|
|
84
|
+
attachFallbackRoutes(app);
|
|
85
|
+
server = app.listen(PORT, () => {
|
|
86
|
+
console.log(`[jenga-api] v${API_VERSION} listening on port ${PORT}`);
|
|
87
|
+
});
|
|
88
|
+
registerShutdownHandlers(server);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ── Graceful Shutdown ──────────────────────────────────────────────────────────
|
|
92
|
+
function registerShutdownHandlers(srv) {
|
|
93
|
+
const FORCE_EXIT_MS = 5000;
|
|
94
|
+
|
|
95
|
+
function shutdown(signal) {
|
|
96
|
+
console.log(`[jenga-api] Received ${signal} — shutting down gracefully…`);
|
|
97
|
+
srv.close(() => {
|
|
98
|
+
console.log('[jenga-api] All connections closed. Exiting cleanly.');
|
|
99
|
+
process.exit(0);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
setTimeout(() => {
|
|
103
|
+
console.error('[jenga-api] Forced exit after 5s timeout.');
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}, FORCE_EXIT_MS).unref();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
109
|
+
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
module.exports = { app, registerShutdownHandlers, attachFallbackRoutes };
|