@latysh-development/monolith 0.0.0-stage → 0.1.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +179 -2
- package/dist/brief/cli.d.ts +30 -0
- package/dist/brief/cli.d.ts.map +1 -0
- package/dist/brief/cli.js +85 -0
- package/dist/brief/collect.d.ts +17 -0
- package/dist/brief/collect.d.ts.map +1 -0
- package/dist/brief/collect.js +342 -0
- package/dist/brief/config.d.ts +40 -0
- package/dist/brief/config.d.ts.map +1 -0
- package/dist/brief/config.js +85 -0
- package/dist/brief/constants.d.ts +12 -0
- package/dist/brief/constants.d.ts.map +1 -0
- package/dist/brief/constants.js +11 -0
- package/dist/brief/docs.d.ts +53 -0
- package/dist/brief/docs.d.ts.map +1 -0
- package/dist/brief/docs.js +229 -0
- package/dist/brief/freshness.d.ts +104 -0
- package/dist/brief/freshness.d.ts.map +1 -0
- package/dist/brief/freshness.js +263 -0
- package/dist/brief/git-signals.d.ts +17 -0
- package/dist/brief/git-signals.d.ts.map +1 -0
- package/dist/brief/git-signals.js +101 -0
- package/dist/brief/index.d.ts +11 -0
- package/dist/brief/index.d.ts.map +1 -0
- package/dist/brief/index.js +5 -0
- package/dist/brief/reconcile.d.ts +75 -0
- package/dist/brief/reconcile.d.ts.map +1 -0
- package/dist/brief/reconcile.js +259 -0
- package/dist/brief/render.d.ts +67 -0
- package/dist/brief/render.d.ts.map +1 -0
- package/dist/brief/render.js +458 -0
- package/dist/brief/sanitize.d.ts +38 -0
- package/dist/brief/sanitize.d.ts.map +1 -0
- package/dist/brief/sanitize.js +48 -0
- package/dist/brief/selectors.d.ts +36 -0
- package/dist/brief/selectors.d.ts.map +1 -0
- package/dist/brief/selectors.js +75 -0
- package/dist/brief/story.d.ts +18 -0
- package/dist/brief/story.d.ts.map +1 -0
- package/dist/brief/story.js +59 -0
- package/dist/brief/types.d.ts +126 -0
- package/dist/brief/types.d.ts.map +1 -0
- package/dist/brief/types.js +11 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +199 -0
- package/dist/cli/init-args.d.ts +16 -0
- package/dist/cli/init-args.d.ts.map +1 -0
- package/dist/cli/init-args.js +24 -0
- package/dist/cli/ui-args.d.ts +23 -0
- package/dist/cli/ui-args.d.ts.map +1 -0
- package/dist/cli/ui-args.js +66 -0
- package/dist/cli/ui.d.ts +40 -0
- package/dist/cli/ui.d.ts.map +1 -0
- package/dist/cli/ui.js +180 -0
- package/dist/coordinatorium/checkpoint.d.ts +44 -0
- package/dist/coordinatorium/checkpoint.d.ts.map +1 -0
- package/dist/coordinatorium/checkpoint.js +124 -0
- package/dist/coordinatorium/cli.d.ts +39 -0
- package/dist/coordinatorium/cli.d.ts.map +1 -0
- package/dist/coordinatorium/cli.js +203 -0
- package/dist/coordinatorium/errors.d.ts +112 -0
- package/dist/coordinatorium/errors.d.ts.map +1 -0
- package/dist/coordinatorium/errors.js +145 -0
- package/dist/coordinatorium/index.d.ts +11 -0
- package/dist/coordinatorium/index.d.ts.map +1 -0
- package/dist/coordinatorium/index.js +5 -0
- package/dist/coordinatorium/lock.d.ts +187 -0
- package/dist/coordinatorium/lock.d.ts.map +1 -0
- package/dist/coordinatorium/lock.js +659 -0
- package/dist/coordinatorium/mission.d.ts +32 -0
- package/dist/coordinatorium/mission.d.ts.map +1 -0
- package/dist/coordinatorium/mission.js +87 -0
- package/dist/coordinatorium/state-keeper.d.ts +115 -0
- package/dist/coordinatorium/state-keeper.d.ts.map +1 -0
- package/dist/coordinatorium/state-keeper.js +396 -0
- package/dist/coordinatorium/types.d.ts +69 -0
- package/dist/coordinatorium/types.d.ts.map +1 -0
- package/dist/coordinatorium/types.js +27 -0
- package/dist/discovery/compose.d.ts +28 -0
- package/dist/discovery/compose.d.ts.map +1 -0
- package/dist/discovery/compose.js +159 -0
- package/dist/discovery/contracts.d.ts +211 -0
- package/dist/discovery/contracts.d.ts.map +1 -0
- package/dist/discovery/contracts.js +555 -0
- package/dist/discovery/docs.d.ts +278 -0
- package/dist/discovery/docs.d.ts.map +1 -0
- package/dist/discovery/docs.js +1182 -0
- package/dist/discovery/facts.d.ts +49 -0
- package/dist/discovery/facts.d.ts.map +1 -0
- package/dist/discovery/facts.js +541 -0
- package/dist/discovery/index.d.ts +8 -0
- package/dist/discovery/index.d.ts.map +1 -0
- package/dist/discovery/index.js +9 -0
- package/dist/discovery/orchestrate.d.ts +35 -0
- package/dist/discovery/orchestrate.d.ts.map +1 -0
- package/dist/discovery/orchestrate.js +32 -0
- package/dist/discovery/render.d.ts +36 -0
- package/dist/discovery/render.d.ts.map +1 -0
- package/dist/discovery/render.js +278 -0
- package/dist/discovery/traverse.d.ts +197 -0
- package/dist/discovery/traverse.d.ts.map +1 -0
- package/dist/discovery/traverse.js +510 -0
- package/dist/durable/assess.d.ts +105 -0
- package/dist/durable/assess.d.ts.map +1 -0
- package/dist/durable/assess.js +239 -0
- package/dist/durable/canonical.d.ts +25 -0
- package/dist/durable/canonical.d.ts.map +1 -0
- package/dist/durable/canonical.js +78 -0
- package/dist/durable/cli.d.ts +37 -0
- package/dist/durable/cli.d.ts.map +1 -0
- package/dist/durable/cli.js +141 -0
- package/dist/durable/errors.d.ts +192 -0
- package/dist/durable/errors.d.ts.map +1 -0
- package/dist/durable/errors.js +323 -0
- package/dist/durable/fs-store.d.ts +254 -0
- package/dist/durable/fs-store.d.ts.map +1 -0
- package/dist/durable/fs-store.js +1493 -0
- package/dist/durable/index.d.ts +20 -0
- package/dist/durable/index.d.ts.map +1 -0
- package/dist/durable/index.js +13 -0
- package/dist/durable/log.d.ts +33 -0
- package/dist/durable/log.d.ts.map +1 -0
- package/dist/durable/log.js +157 -0
- package/dist/durable/memory-store.d.ts +51 -0
- package/dist/durable/memory-store.d.ts.map +1 -0
- package/dist/durable/memory-store.js +302 -0
- package/dist/durable/projection.d.ts +35 -0
- package/dist/durable/projection.d.ts.map +1 -0
- package/dist/durable/projection.js +328 -0
- package/dist/durable/prune-check.d.ts +34 -0
- package/dist/durable/prune-check.d.ts.map +1 -0
- package/dist/durable/prune-check.js +72 -0
- package/dist/durable/register.d.ts +100 -0
- package/dist/durable/register.d.ts.map +1 -0
- package/dist/durable/register.js +532 -0
- package/dist/durable/retention.d.ts +49 -0
- package/dist/durable/retention.d.ts.map +1 -0
- package/dist/durable/retention.js +59 -0
- package/dist/durable/runtime.d.ts +499 -0
- package/dist/durable/runtime.d.ts.map +1 -0
- package/dist/durable/runtime.js +1288 -0
- package/dist/durable/seams.d.ts +44 -0
- package/dist/durable/seams.d.ts.map +1 -0
- package/dist/durable/seams.js +21 -0
- package/dist/durable/testing.d.ts +11 -0
- package/dist/durable/testing.d.ts.map +1 -0
- package/dist/durable/testing.js +25 -0
- package/dist/durable/types.d.ts +442 -0
- package/dist/durable/types.d.ts.map +1 -0
- package/dist/durable/types.js +75 -0
- package/dist/graph/builder.d.ts +11 -0
- package/dist/graph/builder.d.ts.map +1 -0
- package/dist/graph/builder.js +153 -0
- package/dist/graph/identity.d.ts +37 -0
- package/dist/graph/identity.d.ts.map +1 -0
- package/dist/graph/identity.js +46 -0
- package/dist/graph/index.d.ts +7 -0
- package/dist/graph/index.d.ts.map +1 -0
- package/dist/graph/index.js +3 -0
- package/dist/graph/resolver.d.ts +41 -0
- package/dist/graph/resolver.d.ts.map +1 -0
- package/dist/graph/resolver.js +36 -0
- package/dist/graph/types.d.ts +42 -0
- package/dist/graph/types.d.ts.map +1 -0
- package/dist/graph/types.js +1 -0
- package/dist/http/index.d.ts +5 -0
- package/dist/http/index.d.ts.map +1 -0
- package/dist/http/index.js +2 -0
- package/dist/http/server.d.ts +76 -0
- package/dist/http/server.d.ts.map +1 -0
- package/dist/http/server.js +195 -0
- package/dist/http/ui-assets.d.ts +22 -0
- package/dist/http/ui-assets.d.ts.map +1 -0
- package/dist/http/ui-assets.js +100 -0
- package/dist/ideas/cli.d.ts +33 -0
- package/dist/ideas/cli.d.ts.map +1 -0
- package/dist/ideas/cli.js +158 -0
- package/dist/ideas/format.d.ts +38 -0
- package/dist/ideas/format.d.ts.map +1 -0
- package/dist/ideas/format.js +194 -0
- package/dist/ideas/index.d.ts +6 -0
- package/dist/ideas/index.d.ts.map +1 -0
- package/dist/ideas/index.js +3 -0
- package/dist/ideas/store.d.ts +55 -0
- package/dist/ideas/store.d.ts.map +1 -0
- package/dist/ideas/store.js +262 -0
- package/dist/ideas/types.d.ts +54 -0
- package/dist/ideas/types.d.ts.map +1 -0
- package/dist/ideas/types.js +26 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +28 -0
- package/dist/init/index.d.ts +12 -0
- package/dist/init/index.d.ts.map +1 -0
- package/dist/init/index.js +90 -0
- package/dist/init/prompts.d.ts +3 -0
- package/dist/init/prompts.d.ts.map +1 -0
- package/dist/init/prompts.js +47 -0
- package/dist/init/scaffold.d.ts +29 -0
- package/dist/init/scaffold.d.ts.map +1 -0
- package/dist/init/scaffold.js +208 -0
- package/dist/init/types.d.ts +14 -0
- package/dist/init/types.d.ts.map +1 -0
- package/dist/init/types.js +1 -0
- package/dist/managed/index.d.ts +73 -0
- package/dist/managed/index.d.ts.map +1 -0
- package/dist/managed/index.js +222 -0
- package/dist/readmodel/dashboard.d.ts +40 -0
- package/dist/readmodel/dashboard.d.ts.map +1 -0
- package/dist/readmodel/dashboard.js +236 -0
- package/dist/readmodel/index.d.ts +3 -0
- package/dist/readmodel/index.d.ts.map +1 -0
- package/dist/readmodel/index.js +1 -0
- package/dist/readmodel/types.d.ts +104 -0
- package/dist/readmodel/types.d.ts.map +1 -0
- package/dist/readmodel/types.js +15 -0
- package/dist/root/index.d.ts +186 -0
- package/dist/root/index.d.ts.map +1 -0
- package/dist/root/index.js +430 -0
- package/dist/root/safe-write.d.ts +268 -0
- package/dist/root/safe-write.d.ts.map +1 -0
- package/dist/root/safe-write.js +966 -0
- package/dist/root/write-artifacts.d.ts +20 -0
- package/dist/root/write-artifacts.d.ts.map +1 -0
- package/dist/root/write-artifacts.js +42 -0
- package/dist/scriptorium/compile.d.ts +4 -0
- package/dist/scriptorium/compile.d.ts.map +1 -0
- package/dist/scriptorium/compile.js +185 -0
- package/dist/scriptorium/frontmatter.d.ts +13 -0
- package/dist/scriptorium/frontmatter.d.ts.map +1 -0
- package/dist/scriptorium/frontmatter.js +90 -0
- package/dist/scriptorium/index.d.ts +3 -0
- package/dist/scriptorium/index.d.ts.map +1 -0
- package/dist/scriptorium/index.js +1 -0
- package/dist/scriptorium/markdown.d.ts +31 -0
- package/dist/scriptorium/markdown.d.ts.map +1 -0
- package/dist/scriptorium/markdown.js +88 -0
- package/dist/scriptorium/types.d.ts +78 -0
- package/dist/scriptorium/types.d.ts.map +1 -0
- package/dist/scriptorium/types.js +1 -0
- package/dist/types/enforcement.d.ts +11 -0
- package/dist/types/enforcement.d.ts.map +1 -0
- package/dist/types/enforcement.js +1 -0
- package/dist/types/integrity.d.ts +33 -0
- package/dist/types/integrity.d.ts.map +1 -0
- package/dist/types/integrity.js +1 -0
- package/dist/types/project-definition.d.ts +30 -0
- package/dist/types/project-definition.d.ts.map +1 -0
- package/dist/types/project-definition.js +1 -0
- package/dist/ui/assets/cosmic-glass-bg-Dwx47zPu.webp +0 -0
- package/dist/ui/assets/index-C4NhMaq7.js +28 -0
- package/dist/ui/assets/index-DMZ7ZvZY.css +1 -0
- package/dist/ui/index.html +13 -0
- package/dist/update/cli.d.ts +8 -0
- package/dist/update/cli.d.ts.map +1 -0
- package/dist/update/cli.js +115 -0
- package/dist/update/index.d.ts +78 -0
- package/dist/update/index.d.ts.map +1 -0
- package/dist/update/index.js +285 -0
- package/dist/validate/index.d.ts +6 -0
- package/dist/validate/index.d.ts.map +1 -0
- package/dist/validate/index.js +214 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +1 -0
- package/package.json +67 -4
- package/templates/.gitkeep +0 -0
- package/templates/default-instance/instruction/.cursorrules +59 -0
- package/templates/default-instance/instruction/.windsurfrules +59 -0
- package/templates/default-instance/instruction/CLAUDE.md +57 -0
- package/templates/default-instance/managed/agent.md +20 -0
- package/templates/default-instance/managed/bootstrap.md +7 -0
- package/templates/default-instance/managed/index-idea.md +5 -0
- package/templates/default-instance/managed/routing-idea.md +14 -0
- package/templates/default-instance/monolith/AGENT.md +1 -0
- package/templates/default-instance/monolith/context/boot/HANDOFF.md +49 -0
- package/templates/default-instance/monolith/context/boot/WORK_SESSION.md +30 -0
- package/templates/default-instance/monolith/context/execution/current-state.md +30 -0
- package/templates/default-instance/monolith/context/maps/dependency-map.md +38 -0
- package/templates/default-instance/monolith/context/maps/project-map.md +85 -0
- package/templates/default-instance/monolith/context/maps/roadmap-index.md +41 -0
- package/templates/default-instance/monolith/context/maps/routing-map.md +151 -0
- package/templates/default-instance/monolith/context/maps/semantic-map.md +75 -0
- package/templates/default-instance/monolith/context/models/model-registry.md +37 -0
- package/templates/default-instance/monolith/context/models/privacy-classification.md +35 -0
- package/templates/default-instance/monolith/context/templates/plan-template.md +56 -0
- package/templates/default-instance/monolith/governance/README.md +29 -0
- package/templates/default-instance/monolith/governance/RULE_SCHEMA.md +58 -0
- package/templates/default-instance/monolith/learning/INDEX.md +48 -0
- package/templates/default-instance/monolith/learning/injection-map.md +49 -0
- package/templates/default-instance/monolith/learning/taxonomy.md +30 -0
- package/templates/default-instance/monolith/memory/errors/README.md +26 -0
- package/templates/default-instance/monolith/memory/errors/SCHEMA.md +47 -0
- package/templates/default-instance/monolith/memory/errors/TAXONOMY.md +27 -0
- package/templates/default-instance/monolith/memory/long-term/architecture-principles.md +31 -0
- package/templates/default-instance/monolith/memory/long-term/decisions.md +36 -0
- package/templates/default-instance/monolith/memory/long-term/execution-history.md +20 -0
- package/templates/default-instance/monolith/memory/long-term/tech-debt.md +25 -0
- package/templates/default-instance/monolith/memory/working/active.md +41 -0
- package/templates/default-instance/monolith/memory/working/governance-result.md +0 -0
- package/templates/default-instance/monolith/memory/working/integrity-result.md +0 -0
- package/templates/default-instance/monolith/memory/working/story-index.md +28 -0
- package/templates/default-instance/monolith/protocols/boot/boot-sequence.md +59 -0
- package/templates/default-instance/monolith/protocols/cognition/cognitive-modes.md +90 -0
- package/templates/default-instance/monolith/protocols/cognition/dry-run.md +56 -0
- package/templates/default-instance/monolith/protocols/cognition/execution-trace-format.md +52 -0
- package/templates/default-instance/monolith/protocols/cognition/semantic-planning.md +51 -0
- package/templates/default-instance/monolith/protocols/execution/hybrid-execution.md +37 -0
- package/templates/default-instance/monolith/protocols/execution/local-llm-governance.md +42 -0
- package/templates/default-instance/monolith/protocols/governance/command-policy.md +51 -0
- package/templates/default-instance/monolith/protocols/governance/core-invariants.md +40 -0
- package/templates/default-instance/monolith/protocols/governance/memory-governance.md +54 -0
- package/templates/default-instance/monolith/protocols/governance/metadata-schema.md +62 -0
- package/templates/default-instance/monolith/protocols/governance/skills.md +53 -0
- package/templates/default-instance/monolith/protocols/governance/work-session.md +46 -0
- package/templates/default-instance/monolith/skills/INDEX.md +32 -0
- package/templates/default-instance/monolith/skills/current-status/SKILL.md +47 -0
- package/templates/default-instance/monolith/skills/finalize-story/SKILL.md +86 -0
- package/templates/default-instance/monolith/skills/global-status/SKILL.md +56 -0
- package/templates/default-instance/monolith/skills/idea-capture/SKILL.md +157 -0
- package/templates/default-instance/monolith/skills/self-improvement/SKILL.md +77 -0
- package/templates/default-instance/monolith/state.json +11 -0
- package/templates/default-instance/monolith/tests/INDEX.md +57 -0
- package/templates/default-instance/monolith/tests/scenarios/architecture-review.md +32 -0
- package/templates/default-instance/monolith/tests/scenarios/debug-failure.md +32 -0
- package/templates/default-instance/monolith/tests/scenarios/dry-run-activation.md +33 -0
- package/templates/default-instance/monolith/tests/scenarios/execution-trace-verification.md +31 -0
- package/templates/default-instance/monolith/tests/scenarios/finalize-story.md +35 -0
- package/templates/default-instance/monolith/tests/scenarios/implement-feature.md +36 -0
- package/templates/default-instance/monolith/tests/scenarios/local-llm-delegation.md +31 -0
- package/templates/default-instance/monolith/tests/scenarios/planning-request.md +32 -0
- package/templates/default-instance/monolith/tests/scenarios/sensitive-context-task.md +30 -0
- package/templates/default-instance/monolith/tests/scenarios/validator-verification.md +33 -0
- package/templates/default-instance/monolith/tests/validation/expected-behavior.md +70 -0
- package/templates/default-instance/monolith/tests/validators/INDEX.md +17 -0
- package/templates/default-instance/monolith/tests/validators/delegation-semantics.md +22 -0
- package/templates/default-instance/monolith/tests/validators/mode-consistency.md +25 -0
- package/templates/default-instance/monolith/tests/validators/privacy-routing.md +23 -0
- package/templates/default-instance/monolith/tests/validators/protocol-references.md +27 -0
- package/templates/default-instance/monolith/tests/validators/routing-integrity.md +30 -0
- package/templates/default-instance/projectroot/AGENTS.md +111 -0
- package/templates/default-instance/projectroot/monolith.config.json +32 -0
- package/templates/default-instance/projectroot/project.definition.mjs.template +41 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Secure read-only HTTP transport for Mission Control (UI-001B.2, ADR-010 G2).
|
|
3
|
+
*
|
|
4
|
+
* One endpoint, `GET /api/dashboard`, serves the existing `DashboardReadModel`
|
|
5
|
+
* exactly as `readDashboardModel(projectRoot)` returns it. This module is a
|
|
6
|
+
* transport only:
|
|
7
|
+
* - it reads no file and imports no filesystem module; every byte of project
|
|
8
|
+
* data arrives through `readDashboardModel` (ADR-014 safe read);
|
|
9
|
+
* - it knows nothing about `state.json`, its schema or its physical layout, so
|
|
10
|
+
* the Core storage can change (STATE-003) without touching this layer;
|
|
11
|
+
* - the project root is fixed when the server starts and is never taken from a
|
|
12
|
+
* request (query, body, header or cookie are not read for that purpose).
|
|
13
|
+
*
|
|
14
|
+
* With `options.assets` it also serves the built UI from memory (`monolith ui`);
|
|
15
|
+
* the API route, the Host/Origin checks and the loopback bind are unchanged.
|
|
16
|
+
*
|
|
17
|
+
* Security boundary (G2): bound to 127.0.0.1 only; `Host` allowlist (loopback
|
|
18
|
+
* name + the real port, against DNS rebinding); `Origin` checked when present
|
|
19
|
+
* and never reflected; no CORS headers at all; GET only; fixed response
|
|
20
|
+
* headers; bounded response size with no partial JSON; errors are fixed codes,
|
|
21
|
+
* never exception text or paths. Loopback is NOT treated as authentication.
|
|
22
|
+
*
|
|
23
|
+
* Source failures (absent / forbidden / corrupt / unreadable) are part of the
|
|
24
|
+
* DTO (`SourceReport`) and keep their Typed Read Model semantics: they are a
|
|
25
|
+
* valid 200 response carrying `data: null` and a typed code, never project
|
|
26
|
+
* content. Threat model (ADR-013): TM-A supported with limitations, TM-B
|
|
27
|
+
* partially supported, TM-C (concurrent adversarial mutation) NOT supported;
|
|
28
|
+
* dynamic TOCTOU is not closed here, nor by Core.
|
|
29
|
+
*/
|
|
30
|
+
import { createServer } from 'http';
|
|
31
|
+
import { isAbsolute } from 'path';
|
|
32
|
+
import { readDashboardModel } from '../readmodel/index.js';
|
|
33
|
+
export const DASHBOARD_PATH = '/api/dashboard';
|
|
34
|
+
/** The only bind address; there is deliberately no option to change it. */
|
|
35
|
+
export const LOOPBACK_HOST = '127.0.0.1';
|
|
36
|
+
/**
|
|
37
|
+
* Hard ceiling for one serialized response. The Core read limits (state 16 MiB,
|
|
38
|
+
* active.md/roadmap 256 KiB) are protective ceilings of their own and can exceed
|
|
39
|
+
* it; an over-limit DTO is refused whole, never truncated.
|
|
40
|
+
*/
|
|
41
|
+
export const MAX_RESPONSE_BYTES = 2 * 1024 * 1024;
|
|
42
|
+
const ERROR_STATUS = {
|
|
43
|
+
HOST_NOT_ALLOWED: 403,
|
|
44
|
+
ORIGIN_NOT_ALLOWED: 403,
|
|
45
|
+
NOT_FOUND: 404,
|
|
46
|
+
METHOD_NOT_ALLOWED: 405,
|
|
47
|
+
RESPONSE_TOO_LARGE: 500,
|
|
48
|
+
INTERNAL_ERROR: 500,
|
|
49
|
+
};
|
|
50
|
+
const ERROR_KIND = {
|
|
51
|
+
HOST_NOT_ALLOWED: 'security',
|
|
52
|
+
ORIGIN_NOT_ALLOWED: 'security',
|
|
53
|
+
NOT_FOUND: 'request',
|
|
54
|
+
METHOD_NOT_ALLOWED: 'request',
|
|
55
|
+
RESPONSE_TOO_LARGE: 'dashboard',
|
|
56
|
+
INTERNAL_ERROR: 'internal',
|
|
57
|
+
};
|
|
58
|
+
function send(res, status, body, extra = {}) {
|
|
59
|
+
const buf = Buffer.from(body, 'utf8');
|
|
60
|
+
res.writeHead(status, {
|
|
61
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
62
|
+
'Content-Length': String(buf.length),
|
|
63
|
+
'X-Content-Type-Options': 'nosniff',
|
|
64
|
+
'Cache-Control': 'no-store',
|
|
65
|
+
'Content-Security-Policy': "default-src 'none'",
|
|
66
|
+
...extra,
|
|
67
|
+
});
|
|
68
|
+
res.end(buf);
|
|
69
|
+
}
|
|
70
|
+
/** CSP of the UI document and its assets: self-hosted code only, no framing, no form posts. */
|
|
71
|
+
const UI_CSP = "default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; " +
|
|
72
|
+
"font-src 'self' data:; connect-src 'self'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'";
|
|
73
|
+
function sendAsset(res, body, contentType) {
|
|
74
|
+
res.writeHead(200, {
|
|
75
|
+
'Content-Type': contentType,
|
|
76
|
+
'Content-Length': String(body.length),
|
|
77
|
+
'X-Content-Type-Options': 'nosniff',
|
|
78
|
+
'Cache-Control': 'no-store',
|
|
79
|
+
'Content-Security-Policy': UI_CSP,
|
|
80
|
+
'Referrer-Policy': 'no-referrer',
|
|
81
|
+
'Cross-Origin-Resource-Policy': 'same-origin',
|
|
82
|
+
});
|
|
83
|
+
res.end(body);
|
|
84
|
+
}
|
|
85
|
+
function sendError(res, code) {
|
|
86
|
+
const body = { error: { kind: ERROR_KIND[code], code } };
|
|
87
|
+
// An unread request body must not keep the connection busy.
|
|
88
|
+
const extra = { Connection: 'close' };
|
|
89
|
+
if (code === 'METHOD_NOT_ALLOWED')
|
|
90
|
+
extra.Allow = 'GET';
|
|
91
|
+
send(res, ERROR_STATUS[code], JSON.stringify(body), extra);
|
|
92
|
+
}
|
|
93
|
+
function single(h) {
|
|
94
|
+
return typeof h === 'string' ? h : undefined;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Node exposes only the first `Host` in `req.headers`, so duplicates must be
|
|
98
|
+
* counted on the raw header list (names are case-insensitive; values are
|
|
99
|
+
* ignored here and never echoed).
|
|
100
|
+
*/
|
|
101
|
+
function hostHeaderCount(rawHeaders) {
|
|
102
|
+
let count = 0;
|
|
103
|
+
for (let i = 0; i < rawHeaders.length; i += 2) {
|
|
104
|
+
if (rawHeaders[i].toLowerCase() === 'host')
|
|
105
|
+
count++;
|
|
106
|
+
}
|
|
107
|
+
return count;
|
|
108
|
+
}
|
|
109
|
+
/** Path of the request target without its query; the query is never interpreted. */
|
|
110
|
+
function pathOf(target) {
|
|
111
|
+
if (target === undefined)
|
|
112
|
+
return '';
|
|
113
|
+
const q = target.indexOf('?');
|
|
114
|
+
return q === -1 ? target : target.slice(0, q);
|
|
115
|
+
}
|
|
116
|
+
export async function startDashboardServer(options) {
|
|
117
|
+
const { projectRoot } = options;
|
|
118
|
+
if (typeof projectRoot !== 'string' || !isAbsolute(projectRoot)) {
|
|
119
|
+
throw new TypeError('projectRoot must be an absolute path');
|
|
120
|
+
}
|
|
121
|
+
const maxBytes = options.maxResponseBytes ?? MAX_RESPONSE_BYTES;
|
|
122
|
+
let allowedHosts = new Set();
|
|
123
|
+
let allowedOrigins = new Set();
|
|
124
|
+
function handle(req, res) {
|
|
125
|
+
// 1. Security preconditions, before anything about the route is revealed.
|
|
126
|
+
// Exactly one Host: a duplicate is ambiguous (proxies may pick another one).
|
|
127
|
+
const host = single(req.headers.host)?.toLowerCase();
|
|
128
|
+
if (hostHeaderCount(req.rawHeaders) !== 1 || host === undefined || !allowedHosts.has(host))
|
|
129
|
+
return sendError(res, 'HOST_NOT_ALLOWED');
|
|
130
|
+
if (req.headers.origin !== undefined && !allowedOrigins.has(single(req.headers.origin)?.toLowerCase() ?? '')) {
|
|
131
|
+
return sendError(res, 'ORIGIN_NOT_ALLOWED');
|
|
132
|
+
}
|
|
133
|
+
// 2. Route and method. Exact match only: no trailing slash, no decoding.
|
|
134
|
+
const path = pathOf(req.url);
|
|
135
|
+
if (path !== DASHBOARD_PATH) {
|
|
136
|
+
const asset = options.assets?.get(path === '/' ? '/index.html' : path);
|
|
137
|
+
if (asset === undefined)
|
|
138
|
+
return sendError(res, 'NOT_FOUND');
|
|
139
|
+
if (req.method !== 'GET')
|
|
140
|
+
return sendError(res, 'METHOD_NOT_ALLOWED');
|
|
141
|
+
return sendAsset(res, asset.body, asset.contentType);
|
|
142
|
+
}
|
|
143
|
+
if (req.method !== 'GET')
|
|
144
|
+
return sendError(res, 'METHOD_NOT_ALLOWED');
|
|
145
|
+
// 3. The DTO, whole or not at all.
|
|
146
|
+
let body;
|
|
147
|
+
try {
|
|
148
|
+
body = JSON.stringify(readDashboardModel(projectRoot));
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
return sendError(res, 'INTERNAL_ERROR');
|
|
152
|
+
}
|
|
153
|
+
if (Buffer.byteLength(body, 'utf8') > maxBytes)
|
|
154
|
+
return sendError(res, 'RESPONSE_TOO_LARGE');
|
|
155
|
+
send(res, 200, body);
|
|
156
|
+
}
|
|
157
|
+
const server = createServer((req, res) => {
|
|
158
|
+
try {
|
|
159
|
+
handle(req, res);
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
if (!res.headersSent)
|
|
163
|
+
sendError(res, 'INTERNAL_ERROR');
|
|
164
|
+
else
|
|
165
|
+
res.destroy();
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
server.requestTimeout = 10_000;
|
|
169
|
+
server.headersTimeout = 5_000;
|
|
170
|
+
const port = await new Promise((resolve, reject) => {
|
|
171
|
+
server.once('error', reject);
|
|
172
|
+
server.listen(options.port ?? 0, LOOPBACK_HOST, () => {
|
|
173
|
+
server.off('error', reject);
|
|
174
|
+
const addr = server.address();
|
|
175
|
+
if (addr === null || typeof addr === 'string')
|
|
176
|
+
reject(new Error('unexpected listen address'));
|
|
177
|
+
else
|
|
178
|
+
resolve(addr.port);
|
|
179
|
+
});
|
|
180
|
+
}).catch((err) => {
|
|
181
|
+
server.close();
|
|
182
|
+
throw err;
|
|
183
|
+
});
|
|
184
|
+
allowedHosts = new Set([`${LOOPBACK_HOST}:${port}`, `localhost:${port}`]);
|
|
185
|
+
allowedOrigins = new Set([`http://${LOOPBACK_HOST}:${port}`, `http://localhost:${port}`]);
|
|
186
|
+
return {
|
|
187
|
+
host: LOOPBACK_HOST,
|
|
188
|
+
port,
|
|
189
|
+
url: `http://${LOOPBACK_HOST}:${port}`,
|
|
190
|
+
close: () => new Promise((resolve, reject) => {
|
|
191
|
+
server.close((err) => (err ? reject(err) : resolve()));
|
|
192
|
+
server.closeAllConnections();
|
|
193
|
+
}),
|
|
194
|
+
};
|
|
195
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loader for the built Mission Control UI (`dist/ui`), used by `monolith ui`.
|
|
3
|
+
*
|
|
4
|
+
* The HTTP transport (`server.ts`) imports no filesystem module on purpose, so
|
|
5
|
+
* the static files are read here, once, at startup, into memory. The server
|
|
6
|
+
* then serves them by exact key: a request path is never joined to a directory,
|
|
7
|
+
* so path traversal has nothing to traverse.
|
|
8
|
+
*
|
|
9
|
+
* What is accepted: regular files only (no symlink, no special file, one link),
|
|
10
|
+
* a fixed extension allowlist, a bounded directory depth, file count and size.
|
|
11
|
+
* Anything else is skipped, except that a missing or unsafe `index.html` is an
|
|
12
|
+
* error. Assets are read once; later changes on disk are not picked up.
|
|
13
|
+
*/
|
|
14
|
+
import type { UiAssets } from './server.js';
|
|
15
|
+
export type UiAssetsErrorCode = 'UI_BUILD_MISSING' | 'UI_BUILD_UNSAFE' | 'UI_BUILD_TOO_LARGE';
|
|
16
|
+
export declare class UiAssetsError extends Error {
|
|
17
|
+
readonly code: UiAssetsErrorCode;
|
|
18
|
+
constructor(code: UiAssetsErrorCode, message: string);
|
|
19
|
+
}
|
|
20
|
+
/** Reads `dir` (the built UI) into memory. Throws `UiAssetsError` if there is no usable `index.html`. */
|
|
21
|
+
export declare function loadUiAssets(dir: string): UiAssets;
|
|
22
|
+
//# sourceMappingURL=ui-assets.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ui-assets.d.ts","sourceRoot":"","sources":["../../src/http/ui-assets.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAKH,OAAO,KAAK,EAAW,QAAQ,EAAE,MAAM,aAAa,CAAC;AAErD,MAAM,MAAM,iBAAiB,GAAG,kBAAkB,GAAG,iBAAiB,GAAG,oBAAoB,CAAC;AAE9F,qBAAa,aAAc,SAAQ,KAAK;IAC1B,QAAQ,CAAC,IAAI,EAAE,iBAAiB;gBAAvB,IAAI,EAAE,iBAAiB,EAAE,OAAO,EAAE,MAAM;CAI9D;AAiCD,yGAAyG;AACzG,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,QAAQ,CAwClD"}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loader for the built Mission Control UI (`dist/ui`), used by `monolith ui`.
|
|
3
|
+
*
|
|
4
|
+
* The HTTP transport (`server.ts`) imports no filesystem module on purpose, so
|
|
5
|
+
* the static files are read here, once, at startup, into memory. The server
|
|
6
|
+
* then serves them by exact key: a request path is never joined to a directory,
|
|
7
|
+
* so path traversal has nothing to traverse.
|
|
8
|
+
*
|
|
9
|
+
* What is accepted: regular files only (no symlink, no special file, one link),
|
|
10
|
+
* a fixed extension allowlist, a bounded directory depth, file count and size.
|
|
11
|
+
* Anything else is skipped, except that a missing or unsafe `index.html` is an
|
|
12
|
+
* error. Assets are read once; later changes on disk are not picked up.
|
|
13
|
+
*/
|
|
14
|
+
import { closeSync, constants, fstatSync, lstatSync, openSync, readFileSync, readdirSync } from 'fs';
|
|
15
|
+
import { join, extname } from 'path';
|
|
16
|
+
export class UiAssetsError extends Error {
|
|
17
|
+
code;
|
|
18
|
+
constructor(code, message) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.code = code;
|
|
21
|
+
this.name = 'UiAssetsError';
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const CONTENT_TYPES = {
|
|
25
|
+
'.html': 'text/html; charset=utf-8',
|
|
26
|
+
'.js': 'text/javascript; charset=utf-8',
|
|
27
|
+
'.css': 'text/css; charset=utf-8',
|
|
28
|
+
'.webp': 'image/webp',
|
|
29
|
+
'.png': 'image/png',
|
|
30
|
+
'.svg': 'image/svg+xml',
|
|
31
|
+
'.ico': 'image/x-icon',
|
|
32
|
+
'.woff2': 'font/woff2',
|
|
33
|
+
};
|
|
34
|
+
const MAX_DEPTH = 3;
|
|
35
|
+
const MAX_FILES = 200;
|
|
36
|
+
const MAX_FILE_BYTES = 8 * 1024 * 1024;
|
|
37
|
+
const MAX_TOTAL_BYTES = 32 * 1024 * 1024;
|
|
38
|
+
function readRegular(abs) {
|
|
39
|
+
// lstat first so a symlink is refused rather than followed.
|
|
40
|
+
const st = lstatSync(abs);
|
|
41
|
+
if (!st.isFile() || st.nlink !== 1)
|
|
42
|
+
return null;
|
|
43
|
+
if (st.size > MAX_FILE_BYTES)
|
|
44
|
+
throw new UiAssetsError('UI_BUILD_TOO_LARGE', `UI build file is larger than ${MAX_FILE_BYTES} bytes.`);
|
|
45
|
+
const fd = openSync(abs, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
46
|
+
try {
|
|
47
|
+
const fst = fstatSync(fd);
|
|
48
|
+
if (!fst.isFile() || fst.dev !== st.dev || fst.ino !== st.ino)
|
|
49
|
+
return null;
|
|
50
|
+
return readFileSync(fd);
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
closeSync(fd);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/** Reads `dir` (the built UI) into memory. Throws `UiAssetsError` if there is no usable `index.html`. */
|
|
57
|
+
export function loadUiAssets(dir) {
|
|
58
|
+
const assets = new Map();
|
|
59
|
+
let total = 0;
|
|
60
|
+
const walk = (abs, rel, depth) => {
|
|
61
|
+
for (const name of readdirSync(abs).sort()) {
|
|
62
|
+
const childAbs = join(abs, name);
|
|
63
|
+
const childRel = `${rel}/${name}`;
|
|
64
|
+
const st = lstatSync(childAbs);
|
|
65
|
+
if (st.isDirectory()) {
|
|
66
|
+
if (depth < MAX_DEPTH)
|
|
67
|
+
walk(childAbs, childRel, depth + 1);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
const contentType = CONTENT_TYPES[extname(name).toLowerCase()];
|
|
71
|
+
if (contentType === undefined)
|
|
72
|
+
continue;
|
|
73
|
+
const body = readRegular(childAbs);
|
|
74
|
+
if (body === null)
|
|
75
|
+
continue;
|
|
76
|
+
total += body.length;
|
|
77
|
+
if (assets.size >= MAX_FILES || total > MAX_TOTAL_BYTES) {
|
|
78
|
+
throw new UiAssetsError('UI_BUILD_TOO_LARGE', 'UI build has too many or too large files.');
|
|
79
|
+
}
|
|
80
|
+
assets.set(childRel, { body, contentType });
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
try {
|
|
84
|
+
if (!lstatSync(dir).isDirectory())
|
|
85
|
+
throw new UiAssetsError('UI_BUILD_MISSING', 'UI build directory is not a directory.');
|
|
86
|
+
walk(dir, '', 0);
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
if (err instanceof UiAssetsError)
|
|
90
|
+
throw err;
|
|
91
|
+
if (err.code === 'ENOENT') {
|
|
92
|
+
throw new UiAssetsError('UI_BUILD_MISSING', 'UI build directory does not exist.');
|
|
93
|
+
}
|
|
94
|
+
throw new UiAssetsError('UI_BUILD_UNSAFE', 'UI build directory could not be read safely.');
|
|
95
|
+
}
|
|
96
|
+
if (!assets.has('/index.html')) {
|
|
97
|
+
throw new UiAssetsError('UI_BUILD_MISSING', 'UI build has no index.html.');
|
|
98
|
+
}
|
|
99
|
+
return assets;
|
|
100
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `monolith idea ...` command handlers (IDEA-001A).
|
|
3
|
+
*
|
|
4
|
+
* Thin layer: strict flag parsing, store calls, output. No LLM, no network;
|
|
5
|
+
* `add` writes exactly one new file under `<monolithRoot>/memory/long-term/ideas/` and
|
|
6
|
+
* touches no other state (not `active.md`, not `state.json`); `list` and `show` write nothing.
|
|
7
|
+
*/
|
|
8
|
+
import type { Readable } from 'stream';
|
|
9
|
+
interface AddFlags {
|
|
10
|
+
title?: string;
|
|
11
|
+
description?: string;
|
|
12
|
+
context?: string;
|
|
13
|
+
descriptionStdin?: boolean;
|
|
14
|
+
}
|
|
15
|
+
/** Strict: every flag needs a value, none may repeat, unknown flags and stray arguments are errors. */
|
|
16
|
+
export declare function parseAddArgs(args: string[]): AddFlags;
|
|
17
|
+
/**
|
|
18
|
+
* Read all of `input` as UTF-8, byte for byte (no trimming, BOM kept). Refuses
|
|
19
|
+
* more than STDIN_MAX_BYTES, invalid UTF-8 and a read error; nothing is
|
|
20
|
+
* interpreted. A terminal is refused instead of waiting for typed input.
|
|
21
|
+
*/
|
|
22
|
+
export declare function readStdinText(input?: Readable & {
|
|
23
|
+
isTTY?: boolean;
|
|
24
|
+
}): Promise<string>;
|
|
25
|
+
export declare function runIdeaAdd(args: string[], targetDir?: string, stdin?: Readable & {
|
|
26
|
+
isTTY?: boolean;
|
|
27
|
+
}): Promise<void>;
|
|
28
|
+
export declare function runIdeaList(args: string[], targetDir?: string): Promise<void>;
|
|
29
|
+
export declare function runIdeaShow(args: string[], targetDir?: string): Promise<void>;
|
|
30
|
+
/** Dispatcher used by the CLI entrypoint. */
|
|
31
|
+
export declare function runIdeaCommand(args: string[], targetDir?: string): Promise<void>;
|
|
32
|
+
export {};
|
|
33
|
+
//# sourceMappingURL=cli.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/ideas/cli.ts"],"names":[],"mappings":"AACA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AAiBvC,UAAU,QAAQ;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAMD,uGAAuG;AACvG,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,QAAQ,CA4BrD;AAED;;;;GAIG;AACH,wBAAsB,aAAa,CAAC,KAAK,GAAE,QAAQ,GAAG;IAAE,KAAK,CAAC,EAAE,OAAO,CAAA;CAAkB,GAAG,OAAO,CAAC,MAAM,CAAC,CAyB1G;AAED,wBAAsB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,SAAS,SAAgB,EAAE,KAAK,GAAE,QAAQ,GAAG;IAAE,KAAK,CAAC,EAAE,OAAO,CAAA;CAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAYhJ;AAED,wBAAsB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,SAAS,SAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAqB1F;AAiBD,wBAAsB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,SAAS,SAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAG1F;AAED,6CAA6C;AAC7C,wBAAsB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,SAAS,SAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAe7F"}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
/**
|
|
3
|
+
* `monolith idea ...` command handlers (IDEA-001A).
|
|
4
|
+
*
|
|
5
|
+
* Thin layer: strict flag parsing, store calls, output. No LLM, no network;
|
|
6
|
+
* `add` writes exactly one new file under `<monolithRoot>/memory/long-term/ideas/` and
|
|
7
|
+
* touches no other state (not `active.md`, not `state.json`); `list` and `show` write nothing.
|
|
8
|
+
*/
|
|
9
|
+
import { reportWriteWarning } from '../root/index.js';
|
|
10
|
+
import { addIdea, getIdea, ideaStore, listIdeas } from './store.js';
|
|
11
|
+
import { IDEA_LIMITS, IdeaError } from './types.js';
|
|
12
|
+
const ADD_USAGE = 'Usage: monolith idea add --title "..." (--description "..." | --description-stdin) [--context "..."]';
|
|
13
|
+
/** UTF-8 needs at most 4 bytes per UTF-16 code unit's worth of text; anything larger cannot fit the description limit. */
|
|
14
|
+
const STDIN_MAX_BYTES = IDEA_LIMITS.description * 4;
|
|
15
|
+
const LIST_USAGE = 'Usage: monolith idea list';
|
|
16
|
+
const SHOW_USAGE = 'Usage: monolith idea show <id>';
|
|
17
|
+
/** Escape control characters so project-controlled text cannot forge terminal output. */
|
|
18
|
+
function printable(s) {
|
|
19
|
+
// eslint-disable-next-line no-control-regex
|
|
20
|
+
return s.replace(/[\u0000-\u0009\u000b-\u001f\u007f-\u009f\u2028\u2029]/g, (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, '0')}`);
|
|
21
|
+
}
|
|
22
|
+
const ADD_FLAGS = ['--title', '--description', '--context'];
|
|
23
|
+
const STDIN_FLAG = '--description-stdin';
|
|
24
|
+
const KNOWN_FLAGS = [...ADD_FLAGS, STDIN_FLAG];
|
|
25
|
+
/** Strict: every flag needs a value, none may repeat, unknown flags and stray arguments are errors. */
|
|
26
|
+
export function parseAddArgs(args) {
|
|
27
|
+
const out = {};
|
|
28
|
+
for (let i = 0; i < args.length; i++) {
|
|
29
|
+
const flag = args[i];
|
|
30
|
+
if (!KNOWN_FLAGS.includes(flag)) {
|
|
31
|
+
throw new IdeaError('IDEA_INVALID_INPUT', `Unexpected argument ${JSON.stringify(printable(flag))}.\n${ADD_USAGE}`);
|
|
32
|
+
}
|
|
33
|
+
if (flag === STDIN_FLAG) {
|
|
34
|
+
if (out.descriptionStdin)
|
|
35
|
+
throw new IdeaError('IDEA_INVALID_INPUT', `${flag} was given more than once.\n${ADD_USAGE}`);
|
|
36
|
+
out.descriptionStdin = true;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
const key = flag.slice(2);
|
|
40
|
+
if (key in out)
|
|
41
|
+
throw new IdeaError('IDEA_INVALID_INPUT', `${flag} was given more than once.\n${ADD_USAGE}`);
|
|
42
|
+
const value = args[i + 1];
|
|
43
|
+
if (value === undefined || KNOWN_FLAGS.includes(value)) {
|
|
44
|
+
throw new IdeaError('IDEA_INVALID_INPUT', `${flag} needs a value.\n${ADD_USAGE}`);
|
|
45
|
+
}
|
|
46
|
+
out[key] = value;
|
|
47
|
+
i++;
|
|
48
|
+
}
|
|
49
|
+
if (out.descriptionStdin && out.description !== undefined) {
|
|
50
|
+
throw new IdeaError('IDEA_INVALID_INPUT', `--description and ${STDIN_FLAG} cannot be combined.\n${ADD_USAGE}`);
|
|
51
|
+
}
|
|
52
|
+
if (out.title === undefined || (out.description === undefined && !out.descriptionStdin)) {
|
|
53
|
+
throw new IdeaError('IDEA_INVALID_INPUT', `--title and --description (or ${STDIN_FLAG}) are required.\n${ADD_USAGE}`);
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Read all of `input` as UTF-8, byte for byte (no trimming, BOM kept). Refuses
|
|
59
|
+
* more than STDIN_MAX_BYTES, invalid UTF-8 and a read error; nothing is
|
|
60
|
+
* interpreted. A terminal is refused instead of waiting for typed input.
|
|
61
|
+
*/
|
|
62
|
+
export async function readStdinText(input = process.stdin) {
|
|
63
|
+
if (input.isTTY) {
|
|
64
|
+
throw new IdeaError('IDEA_INVALID_INPUT', `${STDIN_FLAG} needs piped input, but stdin is a terminal.\n${ADD_USAGE}`);
|
|
65
|
+
}
|
|
66
|
+
const chunks = [];
|
|
67
|
+
let total = 0;
|
|
68
|
+
try {
|
|
69
|
+
for await (const chunk of input) {
|
|
70
|
+
const buf = typeof chunk === 'string' ? Buffer.from(chunk, 'utf8') : chunk;
|
|
71
|
+
total += buf.length;
|
|
72
|
+
if (total > STDIN_MAX_BYTES) {
|
|
73
|
+
input.destroy();
|
|
74
|
+
throw new IdeaError('IDEA_INVALID_INPUT', `stdin is too large (more than ${STDIN_MAX_BYTES} bytes); nothing was written.`);
|
|
75
|
+
}
|
|
76
|
+
chunks.push(buf);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
if (err instanceof IdeaError)
|
|
81
|
+
throw err;
|
|
82
|
+
throw new IdeaError('IDEA_INVALID_INPUT', `could not read stdin: ${err.message}; nothing was written.`, { cause: err });
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
return new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(Buffer.concat(chunks));
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
throw new IdeaError('IDEA_INVALID_INPUT', 'stdin is not valid UTF-8; nothing was written.', { cause: err });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
export async function runIdeaAdd(args, targetDir = process.cwd(), stdin = process.stdin) {
|
|
92
|
+
const flags = parseAddArgs(args);
|
|
93
|
+
const description = flags.descriptionStdin ? await readStdinText(stdin) : flags.description;
|
|
94
|
+
const store = ideaStore(targetDir);
|
|
95
|
+
const { idea, path } = addIdea(store, { title: flags.title, description, context: flags.context }, { onWarning: (w) => reportWriteWarning(w) });
|
|
96
|
+
console.log(`Saved ${idea.id} [${idea.status}]: ${printable(idea.title)}`);
|
|
97
|
+
console.log(` file: ${path}`);
|
|
98
|
+
console.log('Current task and focus are unchanged. Recall with `monolith idea list` / `monolith idea show ' + idea.id + '`.');
|
|
99
|
+
}
|
|
100
|
+
export async function runIdeaList(args, targetDir = process.cwd()) {
|
|
101
|
+
if (args.length > 0)
|
|
102
|
+
throw new IdeaError('IDEA_INVALID_INPUT', `Unexpected argument ${JSON.stringify(printable(args[0]))}.\n${LIST_USAGE}`);
|
|
103
|
+
const entries = listIdeas(ideaStore(targetDir));
|
|
104
|
+
if (entries.length === 0) {
|
|
105
|
+
console.log('No ideas. Capture one with `monolith idea add --title "..." --description "..."`.');
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
console.log(`Ideas (${entries.length}):`);
|
|
109
|
+
let unreadable = 0;
|
|
110
|
+
for (const e of entries) {
|
|
111
|
+
if (e.ok) {
|
|
112
|
+
console.log(` ${e.idea.id} [${e.idea.status}] ${printable(e.idea.title)}`);
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
unreadable++;
|
|
116
|
+
console.log(` ${e.id} [unreadable] ${printable(e.reason)}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (unreadable > 0) {
|
|
120
|
+
console.error(`${unreadable} idea file(s) could not be read; they were left untouched. Inspect them with \`monolith idea show <id>\`.`);
|
|
121
|
+
process.exitCode = 2;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function formatIdea(i) {
|
|
125
|
+
const lines = [
|
|
126
|
+
` id: ${i.id}`,
|
|
127
|
+
` title: ${printable(i.title)}`,
|
|
128
|
+
` status: ${i.status}`,
|
|
129
|
+
` createdAt: ${i.createdAt}`,
|
|
130
|
+
];
|
|
131
|
+
if (i.tags !== undefined && i.tags.length > 0)
|
|
132
|
+
lines.push(` tags: ${i.tags.map(printable).join(', ')}`);
|
|
133
|
+
lines.push(' context: ' + (i.context === undefined ? '(none)' : ''));
|
|
134
|
+
if (i.context !== undefined)
|
|
135
|
+
lines.push(...i.context.split('\n').map((l) => ` ${l}`));
|
|
136
|
+
lines.push(' description:');
|
|
137
|
+
lines.push(...i.description.split('\n').map((l) => ` ${l}`));
|
|
138
|
+
return lines.join('\n');
|
|
139
|
+
}
|
|
140
|
+
export async function runIdeaShow(args, targetDir = process.cwd()) {
|
|
141
|
+
if (args.length !== 1)
|
|
142
|
+
throw new IdeaError('IDEA_INVALID_INPUT', SHOW_USAGE);
|
|
143
|
+
console.log(formatIdea(getIdea(ideaStore(targetDir), args[0])));
|
|
144
|
+
}
|
|
145
|
+
/** Dispatcher used by the CLI entrypoint. */
|
|
146
|
+
export async function runIdeaCommand(args, targetDir = process.cwd()) {
|
|
147
|
+
const [sub, ...rest] = args;
|
|
148
|
+
switch (sub) {
|
|
149
|
+
case 'add':
|
|
150
|
+
return runIdeaAdd(rest, targetDir);
|
|
151
|
+
case 'list':
|
|
152
|
+
return runIdeaList(rest, targetDir);
|
|
153
|
+
case 'show':
|
|
154
|
+
return runIdeaShow(rest, targetDir);
|
|
155
|
+
default:
|
|
156
|
+
throw new IdeaError('IDEA_INVALID_INPUT', `Unknown idea subcommand: ${sub === undefined ? '(none)' : JSON.stringify(printable(sub))}\n${ADD_USAGE}\n ${LIST_USAGE.slice(7)}\n ${SHOW_USAGE.slice(7)}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Idea file format: Markdown with a small frontmatter block.
|
|
3
|
+
*
|
|
4
|
+
* ---
|
|
5
|
+
* type: idea
|
|
6
|
+
* id: "IDEA-001"
|
|
7
|
+
* title: "..." (JSON string: any character survives a round trip)
|
|
8
|
+
* status: inbox
|
|
9
|
+
* createdAt: "2026-..."
|
|
10
|
+
* context: "..." (optional)
|
|
11
|
+
* tags: ["a", "b"] (optional)
|
|
12
|
+
* ---
|
|
13
|
+
*
|
|
14
|
+
* ## Description
|
|
15
|
+
*
|
|
16
|
+
* <description, verbatim>
|
|
17
|
+
*
|
|
18
|
+
* Strings are written as JSON strings, which are also valid YAML double-quoted
|
|
19
|
+
* scalars, so ERG/Scriptorium read the file too. The reader additionally accepts
|
|
20
|
+
* a plain unquoted value, because a person edits these files by hand.
|
|
21
|
+
* The description is the body: no escaping, no trimming.
|
|
22
|
+
*/
|
|
23
|
+
import { type Idea, type NewIdea } from './types.js';
|
|
24
|
+
export declare function isIdeaId(value: string): boolean;
|
|
25
|
+
export declare function ideaNumber(id: string): number;
|
|
26
|
+
export declare function formatIdeaId(n: number): string;
|
|
27
|
+
/** `IDEA-007.md` -> `IDEA-007`; anything else (temp files, other documents) -> null. */
|
|
28
|
+
export declare function ideaIdFromFileName(name: string): string | null;
|
|
29
|
+
export declare function ideaFileName(id: string): string;
|
|
30
|
+
/** Validates user input; returns it unchanged (content is never trimmed or rewritten). */
|
|
31
|
+
export declare function validateNewIdea(input: NewIdea): NewIdea;
|
|
32
|
+
export declare function serializeIdea(idea: Idea): string;
|
|
33
|
+
/**
|
|
34
|
+
* Parse an idea file. `expectedId` is the id implied by the file name; a
|
|
35
|
+
* mismatch with the `id:` field is corruption, never silently "fixed".
|
|
36
|
+
*/
|
|
37
|
+
export declare function parseIdea(expectedId: string, text: string): Idea;
|
|
38
|
+
//# sourceMappingURL=format.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../../src/ideas/format.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAAyC,KAAK,IAAI,EAAmB,KAAK,OAAO,EAAE,MAAM,YAAY,CAAC;AAS7G,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAE/C;AAED,wBAAgB,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAI7C;AAED,wBAAgB,YAAY,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAE9C;AAED,wFAAwF;AACxF,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAG9D;AAED,wBAAgB,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAG/C;AAaD,0FAA0F;AAC1F,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAmBvD;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,CAahD;AAqBD;;;GAGG;AACH,wBAAgB,SAAS,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CA0DhE"}
|