@jamie-tam/forge 6.0.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/LICENSE +21 -0
- package/README.md +389 -0
- package/agents/architect.md +92 -0
- package/agents/builder.md +122 -0
- package/agents/code-reviewer.md +107 -0
- package/agents/concept-designer.md +207 -0
- package/agents/craft-reviewer.md +132 -0
- package/agents/critic.md +130 -0
- package/agents/doc-writer.md +85 -0
- package/agents/dreamer.md +129 -0
- package/agents/e2e-runner.md +89 -0
- package/agents/gotcha-hunter.md +127 -0
- package/agents/prototype-builder.md +193 -0
- package/agents/prototype-codifier.md +204 -0
- package/agents/prototype-reviewer.md +163 -0
- package/agents/security-reviewer.md +108 -0
- package/agents/spec-reviewer.md +94 -0
- package/agents/tracer.md +98 -0
- package/agents/wireframer.md +109 -0
- package/commands/abort.md +25 -0
- package/commands/bugfix.md +151 -0
- package/commands/evolve.md +118 -0
- package/commands/feature.md +236 -0
- package/commands/forge.md +100 -0
- package/commands/greenfield.md +185 -0
- package/commands/hotfix.md +98 -0
- package/commands/refactor.md +147 -0
- package/commands/resume.md +25 -0
- package/commands/setup.md +201 -0
- package/commands/status.md +27 -0
- package/commands/task-force.md +110 -0
- package/commands/validate.md +12 -0
- package/dist/__tests__/active-manifest.test.js +272 -0
- package/dist/__tests__/copy.test.js +96 -0
- package/dist/__tests__/gate-check.test.js +384 -0
- package/dist/__tests__/wiki.test.js +472 -0
- package/dist/__tests__/work-manifest.test.js +304 -0
- package/dist/active-manifest.js +229 -0
- package/dist/cli.js +158 -0
- package/dist/copy.js +124 -0
- package/dist/gate-check.js +326 -0
- package/dist/hooks.js +60 -0
- package/dist/init.js +140 -0
- package/dist/manifest.js +90 -0
- package/dist/merge.js +77 -0
- package/dist/paths.js +36 -0
- package/dist/uninstall.js +216 -0
- package/dist/update.js +158 -0
- package/dist/verify-manifest.js +65 -0
- package/dist/verify.js +98 -0
- package/dist/wiki-ui.js +310 -0
- package/dist/wiki.js +364 -0
- package/dist/work-manifest.js +798 -0
- package/hooks/config/gate-requirements.json +79 -0
- package/hooks/hooks.json +143 -0
- package/hooks/scripts/analyze-telemetry.sh +114 -0
- package/hooks/scripts/gate-enforcer.sh +164 -0
- package/hooks/scripts/pre-compact.sh +90 -0
- package/hooks/scripts/session-start.sh +81 -0
- package/hooks/scripts/telemetry.sh +41 -0
- package/hooks/scripts/wiki-lint.sh +87 -0
- package/hooks/templates/AGENTS.md.template +48 -0
- package/hooks/templates/CLAUDE.md.template +45 -0
- package/package.json +55 -0
- package/protocols/README.md +40 -0
- package/protocols/codex.md +151 -0
- package/protocols/graphify.md +156 -0
- package/references/common/agent-coordination.md +65 -0
- package/references/common/coding-standards.md +54 -0
- package/references/common/feature-tracking.md +21 -0
- package/references/common/io-protocol.md +36 -0
- package/references/common/phases.md +57 -0
- package/references/common/quality-gates.md +130 -0
- package/references/common/skill-authoring.md +154 -0
- package/references/common/skill-compliance.md +30 -0
- package/references/python/standards.md +44 -0
- package/references/react/standards.md +61 -0
- package/references/typescript/standards.md +42 -0
- package/rules/common/forge-system.md +59 -0
- package/rules/common/git-workflow.md +40 -0
- package/rules/common/guardrails.md +37 -0
- package/rules/common/quality-gates.md +18 -0
- package/rules/common/security.md +50 -0
- package/rules/common/skill-selection.md +78 -0
- package/rules/common/testing.md +58 -0
- package/rules/common/verification.md +39 -0
- package/skills/build-pr-workflow/SKILL.md +301 -0
- package/skills/build-pr-workflow/references/pr-template.md +62 -0
- package/skills/build-pr-workflow/references/subagent-merge.md +47 -0
- package/skills/build-pr-workflow/references/worktree-setup.md +125 -0
- package/skills/build-prototype/SKILL.md +264 -0
- package/skills/build-scaffold/SKILL.md +340 -0
- package/skills/build-tdd/SKILL.md +89 -0
- package/skills/build-wireframe/SKILL.md +110 -0
- package/skills/build-wireframe/assets/baseline-template.html +486 -0
- package/skills/build-wireframe/references/demo-walkthroughs.md +170 -0
- package/skills/build-wireframe/references/gotchas.md +188 -0
- package/skills/build-wireframe/references/legend-lines.md +141 -0
- package/skills/concept-slides/SKILL.md +192 -0
- package/skills/deliver-db-migration/SKILL.md +466 -0
- package/skills/deliver-deploy/SKILL.md +407 -0
- package/skills/deliver-onboarding/SKILL.md +198 -0
- package/skills/deliver-onboarding/references/document-templates.md +393 -0
- package/skills/deliver-onboarding/templates/getting-started.md +122 -0
- package/skills/discover-codebase-analysis/SKILL.md +448 -0
- package/skills/discover-requirements/SKILL.md +418 -0
- package/skills/discover-requirements/templates/prd.md +99 -0
- package/skills/discover-requirements/templates/technical-spec.md +123 -0
- package/skills/discover-requirements/templates/user-stories.md +76 -0
- package/skills/harden/SKILL.md +214 -0
- package/skills/iterate-prototype/SKILL.md +241 -0
- package/skills/plan-architecture/SKILL.md +457 -0
- package/skills/plan-architecture/templates/adr-template.md +52 -0
- package/skills/plan-architecture/templates/api-contract.md +99 -0
- package/skills/plan-architecture/templates/db-schema.md +81 -0
- package/skills/plan-architecture/templates/system-design.md +111 -0
- package/skills/plan-brainstorm/SKILL.md +433 -0
- package/skills/plan-design-system/SKILL.md +279 -0
- package/skills/plan-task-decompose/SKILL.md +454 -0
- package/skills/quality-code-review/SKILL.md +286 -0
- package/skills/quality-security-audit/SKILL.md +292 -0
- package/skills/quality-security-audit/references/audit-report-template.md +89 -0
- package/skills/quality-security-audit/references/owasp-checks.md +178 -0
- package/skills/quality-test-execution/SKILL.md +435 -0
- package/skills/quality-test-plan/SKILL.md +297 -0
- package/skills/quality-test-plan/references/test-type-guide.md +263 -0
- package/skills/quality-test-plan/templates/e2e-test-plan.md +72 -0
- package/skills/quality-test-plan/templates/integration-test-plan.md +74 -0
- package/skills/quality-test-plan/templates/load-test-plan.md +111 -0
- package/skills/quality-test-plan/templates/smoke-test-plan.md +68 -0
- package/skills/quality-test-plan/templates/unit-test-plan.md +56 -0
- package/skills/quality-uiux/SKILL.md +481 -0
- package/skills/support-debug/SKILL.md +464 -0
- package/skills/support-dream/SKILL.md +213 -0
- package/skills/support-gotcha/SKILL.md +249 -0
- package/skills/support-runtime-reachability/SKILL.md +190 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-01-passes-app-use/src/app.ts +7 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-01-passes-app-use/src/handlers/cases.ts +7 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-02-orphan-no-app-use/src/app.ts +8 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-02-orphan-no-app-use/src/handlers/cases.ts +7 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-03-orphan-import-only/src/App.tsx +5 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-03-orphan-import-only/src/components/RingingBanner.tsx +7 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-03-orphan-import-only/src/hooks/useTwilio.ts +6 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-04-jsx-component-rendered/src/App.tsx +5 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-04-jsx-component-rendered/src/components/MyComp.tsx +3 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-05-jsx-component-not-rendered/src/App.tsx +3 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-05-jsx-component-not-rendered/src/components/Orphan.tsx +3 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-06-class-instantiated/src/lib/Service.ts +6 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-06-class-instantiated/src/main.ts +4 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-07-class-not-instantiated/src/lib/Lonely.ts +5 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-07-class-not-instantiated/src/main.ts +2 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-08-default-export-imported-and-called/src/handler.ts +3 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-08-default-export-imported-and-called/src/main.ts +3 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-09-default-export-orphan/src/handler.ts +3 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-09-default-export-orphan/src/main.ts +2 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-10-aliased-named-export/src/lib.ts +5 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-10-aliased-named-export/src/main.ts +3 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-11-re-export-chain/src/lib/index.ts +1 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-11-re-export-chain/src/lib/internal.ts +3 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-11-re-export-chain/src/main.ts +3 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-12-test-only-caller/src/util.test.ts +5 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-12-test-only-caller/src/util.ts +3 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-13-gated-pending-annotation/src/future.ts +4 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-14-untraceable-annotation/src/decorated.ts +4 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-15-untraceable-empty/src/lazy.ts +4 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-16-python-module/src/lib.py +15 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-16-python-module/src/main.py +5 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-17-router-use/src/parent.ts +5 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-17-router-use/src/routes/cases.ts +5 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-18-shadowed-name-fp/src/lib/foo.ts +3 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-18-shadowed-name-fp/src/other.ts +8 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-19-same-name-different-module/src/handlers/cases.ts +4 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-19-same-name-different-module/src/handlers/users.ts +4 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-19-same-name-different-module/src/main.ts +5 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-20-aliased-import-usage/src/handlers/cases.ts +3 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-20-aliased-import-usage/src/main.ts +4 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-21-mixed-default-and-named/src/lib.ts +5 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-21-mixed-default-and-named/src/main.ts +5 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-22-dynamic-import-then-caller/src/lib.ts +3 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-22-dynamic-import-then-caller/src/main.ts +8 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-23-dynamic-import-with-space/src/lib.ts +3 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-23-dynamic-import-with-space/src/main.ts +7 -0
- package/skills/support-runtime-reachability/scripts/check.mjs +638 -0
- package/skills/support-runtime-reachability/scripts/check.test.mjs +244 -0
- package/skills/support-skill-validator/SKILL.md +194 -0
- package/skills/support-skill-validator/references/false-positives.md +59 -0
- package/skills/support-skill-validator/references/validation-checks.md +280 -0
- package/skills/support-system-guide/SKILL.md +311 -0
- package/skills/support-task-force/SKILL.md +265 -0
- package/skills/support-task-force/references/dispatch-pattern.md +178 -0
- package/skills/support-task-force/references/synthesis-template.md +126 -0
- package/skills/support-wiki-bootstrap/SKILL.md +37 -0
- package/skills/support-wiki-lint/SKILL.md +196 -0
- package/skills/support-wiki-lint/scripts/lint.mjs +488 -0
- package/skills/support-wiki-lint/scripts/lint.test.mjs +196 -0
- package/templates/README.md +23 -0
- package/templates/aiwiki/CLAUDE.md.template +78 -0
- package/templates/aiwiki/schemas/architecture.md +118 -0
- package/templates/aiwiki/schemas/convention.md +112 -0
- package/templates/aiwiki/schemas/decision.md +144 -0
- package/templates/aiwiki/schemas/gotcha.md +118 -0
- package/templates/aiwiki/schemas/oracle.md +105 -0
- package/templates/aiwiki/schemas/session.md +125 -0
- package/templates/manifests/bugfix.yaml +41 -0
- package/templates/manifests/feature.yaml +69 -0
- package/templates/manifests/greenfield.yaml +61 -0
- package/templates/manifests/hotfix.yaml +45 -0
- package/templates/manifests/refactor.yaml +44 -0
- package/templates/manifests/v5/SCHEMA.md +327 -0
- package/templates/manifests/v5/feature.yaml +77 -0
- package/templates/manifests/v6/SCHEMA.md +199 -0
- package/templates/wiki-html/dream-detail.html +378 -0
- package/templates/wiki-html/dreams-list.html +155 -0
package/dist/verify.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { targets } from "./paths.js";
|
|
4
|
+
/**
|
|
5
|
+
* Structural verification — checks that installed files are internally
|
|
6
|
+
* consistent. No AI needed, just file existence and JSON validity.
|
|
7
|
+
*/
|
|
8
|
+
export function verify(projectRoot) {
|
|
9
|
+
const t = targets(projectRoot);
|
|
10
|
+
const result = { passed: [], failed: [] };
|
|
11
|
+
// Check gate-requirements.json exists and is valid
|
|
12
|
+
const gateReqPath = join(t.rules, "..", "hooks", "config", "gate-requirements.json");
|
|
13
|
+
if (existsSync(gateReqPath)) {
|
|
14
|
+
try {
|
|
15
|
+
const gates = JSON.parse(readFileSync(gateReqPath, "utf-8"));
|
|
16
|
+
result.passed.push("gate-requirements.json is valid JSON");
|
|
17
|
+
// Check every referenced skill exists
|
|
18
|
+
for (const [gate, config] of Object.entries(gates)) {
|
|
19
|
+
if (gate === "_comment")
|
|
20
|
+
continue;
|
|
21
|
+
const c = config;
|
|
22
|
+
const skill = c.skill;
|
|
23
|
+
if (skill) {
|
|
24
|
+
const skillPath = join(t.skills, skill, "SKILL.md");
|
|
25
|
+
if (existsSync(skillPath)) {
|
|
26
|
+
result.passed.push(`gate ${gate} → skill ${skill} exists`);
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
result.failed.push(`gate ${gate} → skill ${skill} NOT FOUND at ${skillPath}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
// Check agent references
|
|
33
|
+
const agents = [];
|
|
34
|
+
if (c.agent && c.agent !== "null")
|
|
35
|
+
agents.push(c.agent);
|
|
36
|
+
if (Array.isArray(c.agents))
|
|
37
|
+
agents.push(...c.agents);
|
|
38
|
+
for (const agent of agents) {
|
|
39
|
+
const agentPath = join(t.agents, `${agent}.md`);
|
|
40
|
+
if (existsSync(agentPath)) {
|
|
41
|
+
result.passed.push(`gate ${gate} → agent ${agent} exists`);
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
result.failed.push(`gate ${gate} → agent ${agent} NOT FOUND at ${agentPath}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
result.failed.push("gate-requirements.json is invalid JSON");
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
result.failed.push("gate-requirements.json not found");
|
|
55
|
+
}
|
|
56
|
+
// Check hook scripts referenced in settings exist
|
|
57
|
+
const settingsPath = join(t.claude, "settings.local.json");
|
|
58
|
+
if (existsSync(settingsPath)) {
|
|
59
|
+
const settingsStr = readFileSync(settingsPath, "utf-8");
|
|
60
|
+
const scriptRefs = settingsStr.match(/\.claude\/hooks\/scripts\/[^"\\]+/g) ?? [];
|
|
61
|
+
for (const ref of new Set(scriptRefs)) {
|
|
62
|
+
const scriptPath = join(projectRoot, ref);
|
|
63
|
+
if (existsSync(scriptPath)) {
|
|
64
|
+
result.passed.push(`hook script ${ref} exists`);
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
result.failed.push(`hook script ${ref} NOT FOUND`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// Check manifest templates exist
|
|
72
|
+
for (const tpl of ["feature.yaml", "greenfield.yaml"]) {
|
|
73
|
+
const tplPath = join(t.templates, "manifests", tpl);
|
|
74
|
+
if (existsSync(tplPath)) {
|
|
75
|
+
result.passed.push(`manifest template ${tpl} exists`);
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
result.failed.push(`manifest template ${tpl} MISSING at ${tplPath}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// Check core directories exist
|
|
82
|
+
for (const [name, path] of Object.entries({
|
|
83
|
+
agents: t.agents,
|
|
84
|
+
commands: t.commands,
|
|
85
|
+
skills: t.skills,
|
|
86
|
+
rules: t.rules,
|
|
87
|
+
protocols: t.protocols,
|
|
88
|
+
references: t.references,
|
|
89
|
+
})) {
|
|
90
|
+
if (existsSync(path)) {
|
|
91
|
+
result.passed.push(`${name}/ directory exists`);
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
result.failed.push(`${name}/ directory MISSING`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return result;
|
|
98
|
+
}
|
package/dist/wiki-ui.js
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
// Local web UI for reviewing pending wiki dreams. Reads markdown + manifests,
|
|
2
|
+
// renders HTML templates with embedded state, exposes a small JSON API for
|
|
3
|
+
// accept/reject. CSRF-resistant: 127.0.0.1 only, session token in cookie,
|
|
4
|
+
// custom X-Forge-Action header required on mutating endpoints.
|
|
5
|
+
import { randomBytes } from "node:crypto";
|
|
6
|
+
import * as fs from "node:fs";
|
|
7
|
+
import * as http from "node:http";
|
|
8
|
+
import * as path from "node:path";
|
|
9
|
+
import { spawn } from "node:child_process";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
import { acceptDream, getDreamDetails, listDreams, rejectDream, } from "./wiki.js";
|
|
12
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
13
|
+
const __dirname = path.dirname(__filename);
|
|
14
|
+
const TEMPLATES_DIR = path.resolve(__dirname, "..", "templates", "wiki-html");
|
|
15
|
+
const DEFAULT_PORT = 8765;
|
|
16
|
+
const SHUTDOWN_GRACE_MS = 5000;
|
|
17
|
+
export async function wikiUi(opts = {}) {
|
|
18
|
+
const root = await findRepoRoot();
|
|
19
|
+
const stateDir = path.join(root, ".forge", "state");
|
|
20
|
+
fs.mkdirSync(stateDir, { recursive: true });
|
|
21
|
+
const pidFile = path.join(stateDir, "wiki-ui.pid");
|
|
22
|
+
const tokenFile = path.join(stateDir, "wiki-ui-token");
|
|
23
|
+
// Refuse if already running
|
|
24
|
+
if (fs.existsSync(pidFile)) {
|
|
25
|
+
const existingPid = Number(fs.readFileSync(pidFile, "utf8").trim());
|
|
26
|
+
if (Number.isFinite(existingPid) && isProcessAlive(existingPid)) {
|
|
27
|
+
console.error(`forge wiki ui already running (pid ${existingPid}). Stop it first or check the existing browser tab.`);
|
|
28
|
+
return 1;
|
|
29
|
+
}
|
|
30
|
+
// Stale PID file
|
|
31
|
+
fs.unlinkSync(pidFile);
|
|
32
|
+
}
|
|
33
|
+
const token = randomBytes(32).toString("hex");
|
|
34
|
+
fs.writeFileSync(tokenFile, token, { mode: 0o600 });
|
|
35
|
+
const port = await findAvailablePort(opts.port ?? DEFAULT_PORT);
|
|
36
|
+
const server = http.createServer((req, res) => {
|
|
37
|
+
handleRequest(req, res, { root, token }).catch((err) => {
|
|
38
|
+
console.error("Request handler error:", err);
|
|
39
|
+
if (!res.headersSent) {
|
|
40
|
+
res.writeHead(500, { "content-type": "text/plain" });
|
|
41
|
+
res.end("Internal server error");
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
await new Promise((resolve) => {
|
|
46
|
+
server.listen(port, "127.0.0.1", () => {
|
|
47
|
+
resolve();
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
fs.writeFileSync(pidFile, String(process.pid));
|
|
51
|
+
const url = `http://127.0.0.1:${port}/?token=${token}`;
|
|
52
|
+
console.log(`forge wiki ui running at http://127.0.0.1:${port}`);
|
|
53
|
+
console.log(`Press Ctrl-C to stop.`);
|
|
54
|
+
if (!opts.noOpen) {
|
|
55
|
+
openBrowser(url);
|
|
56
|
+
}
|
|
57
|
+
const cleanup = () => {
|
|
58
|
+
try {
|
|
59
|
+
fs.unlinkSync(pidFile);
|
|
60
|
+
}
|
|
61
|
+
catch { }
|
|
62
|
+
try {
|
|
63
|
+
fs.unlinkSync(tokenFile);
|
|
64
|
+
}
|
|
65
|
+
catch { }
|
|
66
|
+
};
|
|
67
|
+
let shuttingDown = false;
|
|
68
|
+
const shutdown = () => {
|
|
69
|
+
if (shuttingDown)
|
|
70
|
+
return;
|
|
71
|
+
shuttingDown = true;
|
|
72
|
+
console.log("\nShutting down...");
|
|
73
|
+
cleanup();
|
|
74
|
+
const timer = setTimeout(() => {
|
|
75
|
+
process.exit(0);
|
|
76
|
+
}, SHUTDOWN_GRACE_MS);
|
|
77
|
+
server.close(() => {
|
|
78
|
+
clearTimeout(timer);
|
|
79
|
+
process.exit(0);
|
|
80
|
+
});
|
|
81
|
+
};
|
|
82
|
+
process.on("SIGINT", shutdown);
|
|
83
|
+
process.on("SIGTERM", shutdown);
|
|
84
|
+
// Block forever (until signal)
|
|
85
|
+
await new Promise(() => { });
|
|
86
|
+
return 0;
|
|
87
|
+
}
|
|
88
|
+
async function handleRequest(req, res, ctx) {
|
|
89
|
+
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
90
|
+
const method = req.method ?? "GET";
|
|
91
|
+
// Auth: query token sets cookie + redirects; otherwise check cookie or header
|
|
92
|
+
const queryToken = url.searchParams.get("token");
|
|
93
|
+
const cookieToken = parseCookie(req.headers.cookie ?? "")["forge_token"];
|
|
94
|
+
const headerToken = req.headers["x-forge-token"];
|
|
95
|
+
if (queryToken && queryToken === ctx.token) {
|
|
96
|
+
// Initial open via auto-launched URL — set cookie + strip query param
|
|
97
|
+
const dest = url.pathname + (url.search ? url.search.replace(/[?&]token=[^&]*/, "").replace(/^&/, "?") : "");
|
|
98
|
+
res.writeHead(302, {
|
|
99
|
+
location: dest === "" ? "/" : dest,
|
|
100
|
+
"set-cookie": `forge_token=${ctx.token}; Path=/; HttpOnly; SameSite=Strict`,
|
|
101
|
+
});
|
|
102
|
+
res.end();
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const authed = cookieToken === ctx.token ||
|
|
106
|
+
(typeof headerToken === "string" && headerToken === ctx.token);
|
|
107
|
+
if (!authed) {
|
|
108
|
+
res.writeHead(401, { "content-type": "text/plain" });
|
|
109
|
+
res.end("Forge wiki ui: invalid or missing token. Re-launch via `forge wiki ui` to get a fresh URL.");
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
// Mutating endpoints: require X-Forge-Action header (CSRF defense)
|
|
113
|
+
if (method === "POST") {
|
|
114
|
+
const actionHeader = req.headers["x-forge-action"];
|
|
115
|
+
if (actionHeader !== "1") {
|
|
116
|
+
res.writeHead(403, { "content-type": "text/plain" });
|
|
117
|
+
res.end("Forbidden: missing X-Forge-Action header");
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const route = `${method} ${url.pathname}`;
|
|
122
|
+
// Static HTML pages
|
|
123
|
+
if (method === "GET" && url.pathname === "/") {
|
|
124
|
+
await serveDreamsListPage(ctx, res);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (method === "GET" && url.pathname.startsWith("/dreams/")) {
|
|
128
|
+
const dreamId = decodeURIComponent(url.pathname.slice("/dreams/".length));
|
|
129
|
+
if (!dreamId) {
|
|
130
|
+
res.writeHead(404, { "content-type": "text/plain" });
|
|
131
|
+
res.end("Not found");
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
await serveDreamDetailPage(ctx, dreamId, res);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
// JSON API
|
|
138
|
+
if (method === "GET" && url.pathname === "/api/state") {
|
|
139
|
+
const dreams = await listDreams(ctx.root);
|
|
140
|
+
respondJson(res, 200, { dreams });
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (method === "GET" && url.pathname.startsWith("/api/dreams/")) {
|
|
144
|
+
const dreamId = decodeURIComponent(url.pathname.slice("/api/dreams/".length));
|
|
145
|
+
const detail = await getDreamDetails(dreamId, ctx.root);
|
|
146
|
+
if (!detail) {
|
|
147
|
+
respondJson(res, 404, { error: "dream_not_found" });
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
respondJson(res, 200, detail);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (method === "POST" &&
|
|
154
|
+
url.pathname.match(/^\/api\/dreams\/[^/]+\/accept$/)) {
|
|
155
|
+
const dreamId = decodeURIComponent(url.pathname.split("/")[3] ?? "");
|
|
156
|
+
try {
|
|
157
|
+
const result = await acceptDream(dreamId, ctx.root);
|
|
158
|
+
respondJson(res, 200, result);
|
|
159
|
+
}
|
|
160
|
+
catch (err) {
|
|
161
|
+
respondJson(res, 400, { error: err.message });
|
|
162
|
+
}
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (method === "POST" &&
|
|
166
|
+
url.pathname.match(/^\/api\/dreams\/[^/]+\/reject$/)) {
|
|
167
|
+
const dreamId = decodeURIComponent(url.pathname.split("/")[3] ?? "");
|
|
168
|
+
const body = await readBody(req);
|
|
169
|
+
let reason = "";
|
|
170
|
+
try {
|
|
171
|
+
reason = JSON.parse(body).reason ?? "";
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
respondJson(res, 400, { error: "invalid_body" });
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
try {
|
|
178
|
+
const result = await rejectDream(dreamId, reason, ctx.root);
|
|
179
|
+
respondJson(res, 200, result);
|
|
180
|
+
}
|
|
181
|
+
catch (err) {
|
|
182
|
+
respondJson(res, 400, { error: err.message });
|
|
183
|
+
}
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
res.writeHead(404, { "content-type": "text/plain" });
|
|
187
|
+
res.end(`Not found: ${route}`);
|
|
188
|
+
}
|
|
189
|
+
// ---- Page rendering --------------------------------------------------------
|
|
190
|
+
async function serveDreamsListPage(ctx, res) {
|
|
191
|
+
const dreams = await listDreams(ctx.root);
|
|
192
|
+
const template = readTemplate("dreams-list.html");
|
|
193
|
+
const html = injectState(template, ctx.token, { dreams });
|
|
194
|
+
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
195
|
+
res.end(html);
|
|
196
|
+
}
|
|
197
|
+
async function serveDreamDetailPage(ctx, dreamId, res) {
|
|
198
|
+
const detail = await getDreamDetails(dreamId, ctx.root);
|
|
199
|
+
if (!detail) {
|
|
200
|
+
res.writeHead(404, { "content-type": "text/plain" });
|
|
201
|
+
res.end(`Dream not found: ${dreamId}`);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
const template = readTemplate("dream-detail.html");
|
|
205
|
+
const html = injectState(template, ctx.token, { dream: detail });
|
|
206
|
+
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
207
|
+
res.end(html);
|
|
208
|
+
}
|
|
209
|
+
function readTemplate(name) {
|
|
210
|
+
return fs.readFileSync(path.join(TEMPLATES_DIR, name), "utf8");
|
|
211
|
+
}
|
|
212
|
+
function injectState(template, token, state) {
|
|
213
|
+
const stateJson = JSON.stringify(state).replace(/</g, "\\u003c");
|
|
214
|
+
return template
|
|
215
|
+
.replace(/\{\{FORGE_TOKEN\}\}/g, escapeHtml(token))
|
|
216
|
+
.replace(/\{\{FORGE_STATE\}\}/g, stateJson);
|
|
217
|
+
}
|
|
218
|
+
function escapeHtml(s) {
|
|
219
|
+
return s.replace(/[&<>"']/g, (c) => {
|
|
220
|
+
const map = {
|
|
221
|
+
"&": "&",
|
|
222
|
+
"<": "<",
|
|
223
|
+
">": ">",
|
|
224
|
+
'"': """,
|
|
225
|
+
"'": "'",
|
|
226
|
+
};
|
|
227
|
+
return map[c] ?? c;
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
function respondJson(res, status, body) {
|
|
231
|
+
res.writeHead(status, { "content-type": "application/json" });
|
|
232
|
+
res.end(JSON.stringify(body));
|
|
233
|
+
}
|
|
234
|
+
async function readBody(req) {
|
|
235
|
+
return new Promise((resolve, reject) => {
|
|
236
|
+
const chunks = [];
|
|
237
|
+
req.on("data", (c) => chunks.push(c));
|
|
238
|
+
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
|
|
239
|
+
req.on("error", reject);
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
function parseCookie(header) {
|
|
243
|
+
const out = {};
|
|
244
|
+
for (const part of header.split(";")) {
|
|
245
|
+
const eq = part.indexOf("=");
|
|
246
|
+
if (eq === -1)
|
|
247
|
+
continue;
|
|
248
|
+
const k = part.slice(0, eq).trim();
|
|
249
|
+
const v = part.slice(eq + 1).trim();
|
|
250
|
+
if (k)
|
|
251
|
+
out[k] = v;
|
|
252
|
+
}
|
|
253
|
+
return out;
|
|
254
|
+
}
|
|
255
|
+
// ---- Lifecycle helpers -----------------------------------------------------
|
|
256
|
+
function isProcessAlive(pid) {
|
|
257
|
+
try {
|
|
258
|
+
process.kill(pid, 0);
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
catch (err) {
|
|
262
|
+
const e = err;
|
|
263
|
+
// EPERM means the process exists but we lack permission — still alive.
|
|
264
|
+
return e.code === "EPERM";
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
async function findAvailablePort(start) {
|
|
268
|
+
for (let port = start; port < start + 100; port++) {
|
|
269
|
+
if (await isPortFree(port))
|
|
270
|
+
return port;
|
|
271
|
+
}
|
|
272
|
+
throw new Error(`No free port found in range ${start}-${start + 100}; try --port`);
|
|
273
|
+
}
|
|
274
|
+
function isPortFree(port) {
|
|
275
|
+
return new Promise((resolve) => {
|
|
276
|
+
const tester = http.createServer();
|
|
277
|
+
tester.once("error", () => resolve(false));
|
|
278
|
+
tester.once("listening", () => {
|
|
279
|
+
tester.close(() => resolve(true));
|
|
280
|
+
});
|
|
281
|
+
tester.listen(port, "127.0.0.1");
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
function openBrowser(url) {
|
|
285
|
+
const platform = process.platform;
|
|
286
|
+
const cmd = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
|
|
287
|
+
const args = platform === "win32" ? ["/c", "start", '""', url] : [url];
|
|
288
|
+
try {
|
|
289
|
+
spawn(cmd, args, { detached: true, stdio: "ignore" }).unref();
|
|
290
|
+
}
|
|
291
|
+
catch (err) {
|
|
292
|
+
console.error(`Could not open browser automatically: ${err.message}`);
|
|
293
|
+
console.error(`Open this URL manually: ${url}`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
async function findRepoRoot() {
|
|
297
|
+
// Walk up from cwd until we find .git or aiwiki/
|
|
298
|
+
let dir = process.cwd();
|
|
299
|
+
for (let i = 0; i < 20; i++) {
|
|
300
|
+
if (fs.existsSync(path.join(dir, ".git")) ||
|
|
301
|
+
fs.existsSync(path.join(dir, "aiwiki"))) {
|
|
302
|
+
return dir;
|
|
303
|
+
}
|
|
304
|
+
const parent = path.dirname(dir);
|
|
305
|
+
if (parent === dir)
|
|
306
|
+
break;
|
|
307
|
+
dir = parent;
|
|
308
|
+
}
|
|
309
|
+
return process.cwd();
|
|
310
|
+
}
|