ndomo 0.1.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/.bun-version +1 -0
- package/.dockerignore +79 -0
- package/.editorconfig +18 -0
- package/.env.example +19 -0
- package/.github/CODEOWNERS +8 -0
- package/.github/ISSUE_TEMPLATE/bug_report.yml +62 -0
- package/.github/ISSUE_TEMPLATE/config.yml +2 -0
- package/.github/ISSUE_TEMPLATE/feature_request.yml +34 -0
- package/.github/dependabot.yml +36 -0
- package/.github/pull_request_template.md +24 -0
- package/.github/release.yml +30 -0
- package/.github/workflows/gitleaks.yml +28 -0
- package/.github/workflows/release-please.yml +27 -0
- package/.github/workflows/smoke.yml +29 -0
- package/.husky/commit-msg +1 -0
- package/CHANGELOG.md +114 -0
- package/Dockerfile +32 -0
- package/README.es.md +174 -0
- package/README.md +187 -0
- package/agents/chronicler.md +98 -0
- package/agents/ci-smith.md +136 -0
- package/agents/craftsman.md +341 -0
- package/agents/deploy-smith.md +138 -0
- package/agents/foreman.md +377 -0
- package/agents/go-smith.md +164 -0
- package/agents/guild.md +188 -0
- package/agents/inspector.md +83 -0
- package/agents/js-smith.md +127 -0
- package/agents/ops-scout.md +173 -0
- package/agents/painter.md +200 -0
- package/agents/python-smith.md +120 -0
- package/agents/ranger.md +307 -0
- package/agents/release-smith.md +165 -0
- package/agents/rust-smith.md +159 -0
- package/agents/sage.md +178 -0
- package/agents/scout.md +144 -0
- package/agents/scribe.md +156 -0
- package/agents/smith.md +201 -0
- package/agents/vue-smith.md +155 -0
- package/agents/warden.md +216 -0
- package/agents/zig-smith.md +156 -0
- package/bin/ndomo-analyses.ts +4 -0
- package/bin/ndomo-status.ts +4 -0
- package/biome.json +57 -0
- package/bun.lock +514 -0
- package/commitlint.config.js +3 -0
- package/config/ndomo.config.json +258 -0
- package/config/ndomo.schema.json +166 -0
- package/docs/agents.md +375 -0
- package/docs/bugs/plan-create-orphan-fk.md +131 -0
- package/docs/bugs/task_create_batch-order-index-collision.md +158 -0
- package/docs/configuration.md +276 -0
- package/docs/database.md +364 -0
- package/docs/features/feature-flexible-builder-v1.md +724 -0
- package/docs/features/feature-flexible-builder-v2.md +882 -0
- package/docs/features/feature-flexible-builder.md +974 -0
- package/docs/http-server.md +244 -0
- package/docs/installation.md +259 -0
- package/docs/integrations.md +129 -0
- package/docs/operations/anti-pattern-sub-agent-verify-2026-06-21.md +32 -0
- package/docs/operations/audit-v1.md +417 -0
- package/docs/operations/audit-v2.md +197 -0
- package/docs/operations/audit-v3.md +306 -0
- package/docs/operations/db-optimize-foundations.md +123 -0
- package/docs/operations/verify-gate-architecture.md +82 -0
- package/docs/workflows.md +448 -0
- package/opencode.json +5 -0
- package/package.json +65 -0
- package/release-please-config.json +11 -0
- package/scripts/dev-bust-cache.sh +164 -0
- package/scripts/install.sh +688 -0
- package/scripts/smoke-e2e.ts +704 -0
- package/scripts/smoke-hot.ts +417 -0
- package/scripts/smoke-http.sh +228 -0
- package/scripts/smoke-v4.ts +256 -0
- package/scripts/smoke-v5.ts +397 -0
- package/scripts/smoke.sh +9 -0
- package/scripts/uninstall.sh +224 -0
- package/skills/api-security-best-practices/SKILL.md +915 -0
- package/skills/bash-scripting/SKILL.md +201 -0
- package/skills/bun/SKILL.md +313 -0
- package/skills/cavecrew/SKILL.md +82 -0
- package/skills/caveman/SKILL.md +74 -0
- package/skills/caveman-review/README.md +33 -0
- package/skills/caveman-review/SKILL.md +55 -0
- package/skills/find-skills/SKILL.md +142 -0
- package/skills/frontend-design/LICENSE.txt +177 -0
- package/skills/frontend-design/SKILL.md +55 -0
- package/skills/golang-patterns/SKILL.md +674 -0
- package/skills/golang-security/SKILL.md +185 -0
- package/skills/golang-security/evals/evals.json +595 -0
- package/skills/golang-security/references/architecture.md +268 -0
- package/skills/golang-security/references/checklist.md +80 -0
- package/skills/golang-security/references/cookies.md +200 -0
- package/skills/golang-security/references/cryptography.md +424 -0
- package/skills/golang-security/references/filesystem.md +285 -0
- package/skills/golang-security/references/injection.md +315 -0
- package/skills/golang-security/references/logging.md +163 -0
- package/skills/golang-security/references/memory-safety.md +241 -0
- package/skills/golang-security/references/network.md +253 -0
- package/skills/golang-security/references/secrets.md +189 -0
- package/skills/golang-security/references/third-party.md +159 -0
- package/skills/golang-security/references/threat-modeling.md +189 -0
- package/skills/golang-testing/SKILL.md +720 -0
- package/skills/grill-me/SKILL.md +7 -0
- package/skills/javascript-testing-patterns/SKILL.md +537 -0
- package/skills/javascript-testing-patterns/references/advanced-testing-patterns.md +513 -0
- package/skills/modern-javascript-patterns/SKILL.md +43 -0
- package/skills/modern-javascript-patterns/references/advanced-patterns.md +487 -0
- package/skills/modern-javascript-patterns/references/details.md +457 -0
- package/skills/python-anti-patterns/SKILL.md +349 -0
- package/skills/python-design-patterns/SKILL.md +85 -0
- package/skills/python-design-patterns/references/details.md +353 -0
- package/skills/python-error-handling/SKILL.md +193 -0
- package/skills/python-error-handling/references/details.md +171 -0
- package/skills/python-testing-patterns/SKILL.md +278 -0
- package/skills/python-testing-patterns/references/advanced-patterns.md +411 -0
- package/skills/python-testing-patterns/references/details.md +349 -0
- package/skills/rust-patterns/SKILL.md +500 -0
- package/skills/rust-testing/SKILL.md +501 -0
- package/skills/security-review/SKILL.md +504 -0
- package/skills/security-review/cloud-infrastructure-security.md +361 -0
- package/skills/vue-best-practices/SKILL.md +154 -0
- package/skills/vue-best-practices/references/animation-class-based-technique.md +254 -0
- package/skills/vue-best-practices/references/animation-state-driven-technique.md +291 -0
- package/skills/vue-best-practices/references/component-async.md +97 -0
- package/skills/vue-best-practices/references/component-data-flow.md +307 -0
- package/skills/vue-best-practices/references/component-fallthrough-attrs.md +174 -0
- package/skills/vue-best-practices/references/component-keep-alive.md +137 -0
- package/skills/vue-best-practices/references/component-slots.md +216 -0
- package/skills/vue-best-practices/references/component-suspense.md +228 -0
- package/skills/vue-best-practices/references/component-teleport.md +108 -0
- package/skills/vue-best-practices/references/component-transition-group.md +128 -0
- package/skills/vue-best-practices/references/component-transition.md +125 -0
- package/skills/vue-best-practices/references/composables.md +290 -0
- package/skills/vue-best-practices/references/directives.md +162 -0
- package/skills/vue-best-practices/references/perf-avoid-component-abstraction-in-lists.md +159 -0
- package/skills/vue-best-practices/references/perf-v-once-v-memo-directives.md +182 -0
- package/skills/vue-best-practices/references/perf-virtualize-large-lists.md +187 -0
- package/skills/vue-best-practices/references/plugins.md +166 -0
- package/skills/vue-best-practices/references/reactivity.md +344 -0
- package/skills/vue-best-practices/references/render-functions.md +201 -0
- package/skills/vue-best-practices/references/sfc.md +310 -0
- package/skills/vue-best-practices/references/state-management.md +135 -0
- package/skills/vue-best-practices/references/updated-hook-performance.md +187 -0
- package/skills/vue-pinia-best-practices/SKILL.md +21 -0
- package/skills/vue-pinia-best-practices/reference/pinia-no-active-pinia-error.md +248 -0
- package/skills/vue-pinia-best-practices/reference/pinia-setup-store-return-all-state.md +227 -0
- package/skills/vue-pinia-best-practices/reference/pinia-store-destructuring-breaks-reactivity.md +193 -0
- package/skills/vue-pinia-best-practices/reference/state-url-for-ephemeral-filters.md +238 -0
- package/skills/vue-pinia-best-practices/reference/state-use-pinia-for-large-apps.md +262 -0
- package/skills/vue-pinia-best-practices/reference/store-method-binding-parentheses.md +191 -0
- package/skills/zig-0.16/SKILL.md +840 -0
- package/skills/zig-0.16/scripts/check-zig-version.sh +21 -0
- package/src/cli/analyses.ts +280 -0
- package/src/cli/index.ts +108 -0
- package/src/cli/serve.ts +192 -0
- package/src/cli/smoke.ts +131 -0
- package/src/cli/status.test.ts +204 -0
- package/src/cli/status.ts +263 -0
- package/src/cli/vacuum.test.ts +82 -0
- package/src/cli/vacuum.ts +96 -0
- package/src/config/schema.test.ts +88 -0
- package/src/config/schema.ts +64 -0
- package/src/db/analyses-migration.test.ts +210 -0
- package/src/db/analyses.test.ts +466 -0
- package/src/db/analyses.ts +375 -0
- package/src/db/auto-checkpoint.ts +131 -0
- package/src/db/client.test.ts +129 -0
- package/src/db/client.ts +55 -0
- package/src/db/fts-escape.ts +20 -0
- package/src/db/incidents.test.ts +201 -0
- package/src/db/incidents.ts +93 -0
- package/src/db/index.ts +86 -0
- package/src/db/migrations-v13.test.ts +141 -0
- package/src/db/migrations-v8.test.ts +301 -0
- package/src/db/migrations.ts +147 -0
- package/src/db/plan-archive.test.ts +180 -0
- package/src/db/plan-archive.ts +274 -0
- package/src/db/plan-create.test.ts +276 -0
- package/src/db/plan-create.ts +78 -0
- package/src/db/plan-files.test.ts +289 -0
- package/src/db/plan-update-status.ts +287 -0
- package/src/db/plans.test.ts +490 -0
- package/src/db/plans.ts +534 -0
- package/src/db/resolve-project-dir.test.ts +143 -0
- package/src/db/resolve-project-dir.ts +75 -0
- package/src/db/rollbacks.test.ts +150 -0
- package/src/db/rollbacks.ts +67 -0
- package/src/db/schema.ts +907 -0
- package/src/db/sessions.test.ts +80 -0
- package/src/db/sessions.ts +135 -0
- package/src/db/shutdown.test.ts +147 -0
- package/src/db/shutdown.ts +45 -0
- package/src/db/tasks.test.ts +921 -0
- package/src/db/tasks.ts +747 -0
- package/src/db/types.ts +619 -0
- package/src/http/__tests__/auth.test.ts +196 -0
- package/src/http/__tests__/routes.test.ts +465 -0
- package/src/http/__tests__/sse.test.ts +317 -0
- package/src/http/auth.ts +72 -0
- package/src/http/middleware/cors.ts +53 -0
- package/src/http/middleware/security-headers.ts +21 -0
- package/src/http/routes/events.ts +112 -0
- package/src/http/routes/health.ts +51 -0
- package/src/http/routes/plans.ts +66 -0
- package/src/http/routes/sessions.ts +50 -0
- package/src/http/routes/tasks.ts +60 -0
- package/src/http/server.ts +95 -0
- package/src/http/sse.ts +116 -0
- package/src/index.ts +37 -0
- package/src/lib.ts +65 -0
- package/src/mem/scoped.ts +65 -0
- package/src/orchestrator/background.test.ts +268 -0
- package/src/orchestrator/background.ts +293 -0
- package/src/orchestrator/memory-hook.ts +182 -0
- package/src/orchestrator/reconciler.ts +123 -0
- package/src/orchestrator/scheduler.test.ts +300 -0
- package/src/orchestrator/scheduler.ts +243 -0
- package/src/plugin.test.ts +2574 -0
- package/src/plugin.ts +1690 -0
- package/src/sdk/client.ts +66 -0
- package/src/worktrees/manager.ts +236 -0
- package/src/worktrees/state.ts +87 -0
- package/tests/integration/ranger-flow.test.ts +257 -0
- package/tools/analysis_archive.ts +28 -0
- package/tools/analysis_create.ts +55 -0
- package/tools/analysis_get.ts +33 -0
- package/tools/analysis_link_plan.ts +44 -0
- package/tools/analysis_list.ts +48 -0
- package/tools/analysis_search.ts +36 -0
- package/tools/analysis_update.ts +44 -0
- package/tools/plan_approve.ts +31 -0
- package/tools/plan_create.ts +58 -0
- package/tools/plan_get.ts +40 -0
- package/tools/plan_list.ts +37 -0
- package/tools/plan_search.ts +34 -0
- package/tools/plan_update_status.ts +71 -0
- package/tools/session_checkpoint.ts +31 -0
- package/tools/session_end.ts +26 -0
- package/tools/session_start.ts +43 -0
- package/tools/task_create_batch.ts +70 -0
- package/tools/task_list.ts +35 -0
- package/tools/task_next_for_agent.ts +30 -0
- package/tools/task_search.ts +34 -0
- package/tools/task_update_status.ts +37 -0
- package/tsconfig.json +31 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Verify Zig 0.16.x is available
|
|
3
|
+
set -e
|
|
4
|
+
|
|
5
|
+
ZIG_CMD="${ZIG_CMD:-zig}"
|
|
6
|
+
|
|
7
|
+
if ! command -v "$ZIG_CMD" &> /dev/null; then
|
|
8
|
+
echo "ERROR: zig not found. Set ZIG_CMD or add zig to PATH."
|
|
9
|
+
exit 1
|
|
10
|
+
fi
|
|
11
|
+
|
|
12
|
+
VERSION=$("$ZIG_CMD" version)
|
|
13
|
+
MAJOR=$(echo "$VERSION" | cut -d. -f1)
|
|
14
|
+
MINOR=$(echo "$VERSION" | cut -d. -f2)
|
|
15
|
+
|
|
16
|
+
if [[ "$MAJOR" -eq 0 && "$MINOR" -ge 16 ]] || [[ "$MAJOR" -ge 1 ]]; then
|
|
17
|
+
echo "OK: Zig $VERSION detected."
|
|
18
|
+
else
|
|
19
|
+
echo "WARNING: Zig $VERSION detected, this skill is for 0.16+."
|
|
20
|
+
exit 1
|
|
21
|
+
fi
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* ndomo analyses CLI — list, get, search, archive analyses.
|
|
4
|
+
*
|
|
5
|
+
* Reads .ndomo/state.db from the project root (resolved same as status.ts).
|
|
6
|
+
* Supports list/get/search/archive subcommands with filters.
|
|
7
|
+
*
|
|
8
|
+
* Uses bun:sqlite (synchronous) — no async/await on DB ops.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { Database } from "bun:sqlite";
|
|
12
|
+
import { existsSync } from "node:fs";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import {
|
|
15
|
+
archiveAnalysis,
|
|
16
|
+
getAnalysis,
|
|
17
|
+
getAnalysisBySlug,
|
|
18
|
+
listAnalyses,
|
|
19
|
+
searchAnalyses,
|
|
20
|
+
} from "../db/analyses.ts";
|
|
21
|
+
import { runMigrations } from "../db/migrations.ts";
|
|
22
|
+
import type { Analysis } from "../db/types.ts";
|
|
23
|
+
|
|
24
|
+
const NDOMO_DIR = ".ndomo";
|
|
25
|
+
const DB_FILE = "state.db";
|
|
26
|
+
|
|
27
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
/** Resolve DB path — same logic as src/cli/status.ts. */
|
|
30
|
+
function resolveDbPath(): string | null {
|
|
31
|
+
const cwdPath = join(process.cwd(), NDOMO_DIR, DB_FILE);
|
|
32
|
+
if (existsSync(cwdPath)) return cwdPath;
|
|
33
|
+
|
|
34
|
+
let dir = process.cwd();
|
|
35
|
+
for (let i = 0; i < 5; i++) {
|
|
36
|
+
const parent = join(dir, "..");
|
|
37
|
+
if (parent === dir) break;
|
|
38
|
+
dir = parent;
|
|
39
|
+
const candidate = join(dir, NDOMO_DIR, DB_FILE);
|
|
40
|
+
if (existsSync(candidate)) return candidate;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Short ID — first 8 chars. */
|
|
47
|
+
function shortId(id: string): string {
|
|
48
|
+
return id.slice(0, 8);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Truncate string to maxLen with "..." suffix. */
|
|
52
|
+
function truncate(s: string, maxLen: number): string {
|
|
53
|
+
if (s.length <= maxLen) return s;
|
|
54
|
+
return `${s.slice(0, maxLen - 3)}...`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Print analyses as table. */
|
|
58
|
+
function printTable(analyses: Analysis[]): void {
|
|
59
|
+
if (analyses.length === 0) {
|
|
60
|
+
console.log("no analyses found");
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
console.log(
|
|
65
|
+
` ${"id".padEnd(10)}${"slug".padEnd(26)}${"title".padEnd(32)}${"agent".padEnd(12)}${"sourcePlan".padEnd(12)}${"updatedAt".padEnd(22)}`,
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
for (const a of analyses) {
|
|
69
|
+
const id = shortId(a.id);
|
|
70
|
+
const slug = truncate(a.slug, 24);
|
|
71
|
+
const title = truncate(a.title, 30);
|
|
72
|
+
const agent = truncate(a.agent, 10);
|
|
73
|
+
const sourcePlan = a.sourcePlanId ? shortId(a.sourcePlanId) : "-";
|
|
74
|
+
const updatedAt = a.updatedAt ?? "-";
|
|
75
|
+
|
|
76
|
+
console.log(
|
|
77
|
+
` ${id.padEnd(10)}${slug.padEnd(26)}${title.padEnd(32)}${agent.padEnd(12)}${sourcePlan.padEnd(12)}${updatedAt.padEnd(22)}`,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Print single analysis as JSON. */
|
|
83
|
+
function printJson(data: unknown): void {
|
|
84
|
+
console.log(JSON.stringify(data, null, 2));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Print help text. */
|
|
88
|
+
function printHelp(): void {
|
|
89
|
+
console.log(`Usage: ndomo analyses <subcommand> [options]
|
|
90
|
+
|
|
91
|
+
Subcommands:
|
|
92
|
+
list List analyses (default)
|
|
93
|
+
--agent <name> Filter by agent
|
|
94
|
+
--source-plan <id> Filter by source plan ID
|
|
95
|
+
--project <path> Filter by project path
|
|
96
|
+
--archived Include archived analyses
|
|
97
|
+
--limit <n> Max results (default 50)
|
|
98
|
+
|
|
99
|
+
get <id-or-slug> Get analysis by ID or slug
|
|
100
|
+
--project <path> Required when using slug
|
|
101
|
+
|
|
102
|
+
search <query> Full-text search over title+summary+findings
|
|
103
|
+
--limit <n> Max results (default 20)
|
|
104
|
+
|
|
105
|
+
archive <id> Soft-delete an analysis
|
|
106
|
+
|
|
107
|
+
help Show this help`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ─── Subcommand handlers ─────────────────────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
function handleList(db: Database, args: string[]): void {
|
|
113
|
+
let agent: string | undefined;
|
|
114
|
+
let sourcePlanId: string | undefined;
|
|
115
|
+
let projectPath: string | undefined;
|
|
116
|
+
let archived: boolean | undefined;
|
|
117
|
+
let limit: number | undefined;
|
|
118
|
+
|
|
119
|
+
for (let i = 0; i < args.length; i++) {
|
|
120
|
+
const arg = args[i]!;
|
|
121
|
+
if (arg === "--agent" && i + 1 < args.length) {
|
|
122
|
+
agent = args[++i]!;
|
|
123
|
+
} else if (arg === "--source-plan" && i + 1 < args.length) {
|
|
124
|
+
sourcePlanId = args[++i]!;
|
|
125
|
+
} else if (arg === "--project" && i + 1 < args.length) {
|
|
126
|
+
projectPath = args[++i]!;
|
|
127
|
+
} else if (arg === "--archived") {
|
|
128
|
+
archived = true;
|
|
129
|
+
} else if (arg === "--limit" && i + 1 < args.length) {
|
|
130
|
+
const n = Number.parseInt(args[++i]!, 10);
|
|
131
|
+
if (Number.isNaN(n) || n < 1) {
|
|
132
|
+
console.error("error: --limit must be a positive integer");
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
limit = n;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const filters: Parameters<typeof listAnalyses>[1] = {};
|
|
140
|
+
if (agent !== undefined) filters.agent = agent;
|
|
141
|
+
if (sourcePlanId !== undefined) filters.sourcePlanId = sourcePlanId;
|
|
142
|
+
if (projectPath !== undefined) filters.projectPath = projectPath;
|
|
143
|
+
if (archived !== undefined) filters.archived = archived;
|
|
144
|
+
if (limit !== undefined) filters.limit = limit;
|
|
145
|
+
|
|
146
|
+
const results = listAnalyses(db, filters);
|
|
147
|
+
printTable(results);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function handleGet(db: Database, args: string[]): void {
|
|
151
|
+
if (args.length === 0) {
|
|
152
|
+
console.error("error: get requires an ID or slug argument");
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const identifier = args[0]!;
|
|
157
|
+
let projectPath: string | undefined;
|
|
158
|
+
|
|
159
|
+
for (let i = 1; i < args.length; i++) {
|
|
160
|
+
if (args[i] === "--project" && i + 1 < args.length) {
|
|
161
|
+
projectPath = args[++i]!;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
let analysis: Analysis | null = null;
|
|
166
|
+
|
|
167
|
+
// UUIDs have 8-4-4-4-12 format; slugs are shorter kebab-case
|
|
168
|
+
const isUuid = identifier.includes("-") && identifier.length >= 36;
|
|
169
|
+
|
|
170
|
+
if (isUuid) {
|
|
171
|
+
analysis = getAnalysis(db, identifier);
|
|
172
|
+
} else {
|
|
173
|
+
// Treat as slug — requires --project
|
|
174
|
+
if (projectPath === undefined) {
|
|
175
|
+
console.error("error: --project <path> is required when using slug");
|
|
176
|
+
process.exit(1);
|
|
177
|
+
}
|
|
178
|
+
analysis = getAnalysisBySlug(db, identifier, projectPath);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (!analysis) {
|
|
182
|
+
console.error(`error: analysis '${identifier}' not found`);
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
printJson(analysis);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function handleSearch(db: Database, args: string[]): void {
|
|
190
|
+
if (args.length === 0) {
|
|
191
|
+
console.error("error: search requires a query argument");
|
|
192
|
+
process.exit(1);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const query = args[0]!;
|
|
196
|
+
let limit = 20;
|
|
197
|
+
|
|
198
|
+
for (let i = 1; i < args.length; i++) {
|
|
199
|
+
if (args[i] === "--limit" && i + 1 < args.length) {
|
|
200
|
+
const n = Number.parseInt(args[++i]!, 10);
|
|
201
|
+
if (Number.isNaN(n) || n < 1) {
|
|
202
|
+
console.error("error: --limit must be a positive integer");
|
|
203
|
+
process.exit(1);
|
|
204
|
+
}
|
|
205
|
+
limit = n;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const results = searchAnalyses(db, query, { limit });
|
|
210
|
+
printTable(results);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function handleArchive(db: Database, args: string[]): void {
|
|
214
|
+
if (args.length === 0) {
|
|
215
|
+
console.error("error: archive requires an ID argument");
|
|
216
|
+
process.exit(1);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const id = args[0]!;
|
|
220
|
+
try {
|
|
221
|
+
const archived = archiveAnalysis(db, id);
|
|
222
|
+
console.log(`archived analysis: ${shortId(archived.id)} (${archived.slug})`);
|
|
223
|
+
} catch (err) {
|
|
224
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
225
|
+
console.error(`error: ${message}`);
|
|
226
|
+
process.exit(1);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ─── Main ────────────────────────────────────────────────────────────────────
|
|
231
|
+
|
|
232
|
+
export function runAnalyses(args: string[]): void {
|
|
233
|
+
const subcommand = args[0];
|
|
234
|
+
|
|
235
|
+
// Help (before DB resolution)
|
|
236
|
+
if (!subcommand || subcommand === "help" || subcommand === "--help" || subcommand === "-h") {
|
|
237
|
+
printHelp();
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const dbPath = resolveDbPath();
|
|
242
|
+
if (!dbPath) {
|
|
243
|
+
console.error("error: .ndomo/state.db not found — run from project root or parent dir");
|
|
244
|
+
process.exit(1);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const db = new Database(dbPath);
|
|
248
|
+
db.exec("PRAGMA foreign_keys = ON");
|
|
249
|
+
runMigrations(db);
|
|
250
|
+
|
|
251
|
+
try {
|
|
252
|
+
const rest = args.slice(1);
|
|
253
|
+
|
|
254
|
+
switch (subcommand) {
|
|
255
|
+
case "list":
|
|
256
|
+
handleList(db, rest);
|
|
257
|
+
break;
|
|
258
|
+
case "get":
|
|
259
|
+
handleGet(db, rest);
|
|
260
|
+
break;
|
|
261
|
+
case "search":
|
|
262
|
+
handleSearch(db, rest);
|
|
263
|
+
break;
|
|
264
|
+
case "archive":
|
|
265
|
+
handleArchive(db, rest);
|
|
266
|
+
break;
|
|
267
|
+
default:
|
|
268
|
+
console.error(`error: unknown subcommand '${subcommand}'`);
|
|
269
|
+
printHelp();
|
|
270
|
+
process.exit(1);
|
|
271
|
+
}
|
|
272
|
+
} finally {
|
|
273
|
+
db.close();
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Direct execution
|
|
278
|
+
if (import.meta.main) {
|
|
279
|
+
runAnalyses(process.argv.slice(2));
|
|
280
|
+
}
|
package/src/cli/index.ts
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* ndomo CLI — unified entry point for all subcommands.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* bun run src/cli/index.ts <command> [options]
|
|
7
|
+
*
|
|
8
|
+
* Commands:
|
|
9
|
+
* status Show plans grouped by status with task counts
|
|
10
|
+
* serve Start the HTTP server
|
|
11
|
+
* vacuum Reclaim disk space from .ndomo/state.db
|
|
12
|
+
* smoke Run smoke tests
|
|
13
|
+
* help Show this help
|
|
14
|
+
*
|
|
15
|
+
* Each subcommand can also be run directly:
|
|
16
|
+
* bun run src/cli/status.ts --plans
|
|
17
|
+
* bun run src/cli/serve.ts --port 8080
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const COMMANDS: Record<
|
|
21
|
+
string,
|
|
22
|
+
{ description: string; run: (args: string[]) => void | Promise<void> }
|
|
23
|
+
> = {
|
|
24
|
+
status: {
|
|
25
|
+
description: "Show plans grouped by status with task counts",
|
|
26
|
+
run: async (args) => {
|
|
27
|
+
const { runStatus } = await import("./status.ts");
|
|
28
|
+
runStatus(args);
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
serve: {
|
|
32
|
+
description: "Start the HTTP server",
|
|
33
|
+
run: async (args) => {
|
|
34
|
+
const { runServe } = await import("./serve.ts");
|
|
35
|
+
await runServe(args);
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
vacuum: {
|
|
39
|
+
description: "Reclaim disk space from .ndomo/state.db",
|
|
40
|
+
run: async (args) => {
|
|
41
|
+
const { vacuumProject } = await import("./vacuum.ts");
|
|
42
|
+
const projectDir = args[0] ?? process.cwd();
|
|
43
|
+
const result = vacuumProject(projectDir);
|
|
44
|
+
const delta = result.sizeBefore - result.sizeAfter;
|
|
45
|
+
console.log(`[vacuum] incremental_vacuum: reclaimed ${result.pagesReclaimed} pages`);
|
|
46
|
+
console.log(`[vacuum] wal_checkpoint(TRUNCATE): ${JSON.stringify(result.checkpoint)}`);
|
|
47
|
+
console.log(
|
|
48
|
+
`[vacuum] file size: ${result.sizeBefore} → ${result.sizeAfter} bytes (${delta >= 0 ? "-" : "+"}${Math.abs(delta)} bytes)`,
|
|
49
|
+
);
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
smoke: {
|
|
53
|
+
description: "Run smoke tests",
|
|
54
|
+
run: async () => {
|
|
55
|
+
// smoke.ts runs on import — just import it
|
|
56
|
+
await import("./smoke.ts");
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
function printHelp(): void {
|
|
62
|
+
console.log(`ndomo CLI — multi-agent plugin
|
|
63
|
+
|
|
64
|
+
Usage:
|
|
65
|
+
bun run src/cli/index.ts <command> [options]
|
|
66
|
+
|
|
67
|
+
Commands:`);
|
|
68
|
+
|
|
69
|
+
for (const [name, cmd] of Object.entries(COMMANDS)) {
|
|
70
|
+
console.log(` ${name.padEnd(12)}${cmd.description}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
console.log(`
|
|
74
|
+
Each subcommand can also be run directly:
|
|
75
|
+
bun run src/cli/status.ts --plans
|
|
76
|
+
bun run src/cli/serve.ts --port 8080
|
|
77
|
+
bun run src/cli/vacuum.ts`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function main(): Promise<void> {
|
|
81
|
+
const args = process.argv.slice(2);
|
|
82
|
+
const commandName = args[0];
|
|
83
|
+
|
|
84
|
+
if (!commandName || commandName === "--help" || commandName === "-h" || commandName === "help") {
|
|
85
|
+
printHelp();
|
|
86
|
+
process.exit(commandName ? 0 : 1);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const command = COMMANDS[commandName];
|
|
90
|
+
if (!command) {
|
|
91
|
+
console.error(
|
|
92
|
+
`error: unknown command "${commandName}". Run "ndomo help" for available commands.`,
|
|
93
|
+
);
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
await command.run(args.slice(1));
|
|
99
|
+
} catch (err) {
|
|
100
|
+
console.error(`error: ${err instanceof Error ? err.message : String(err)}`);
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Direct execution
|
|
106
|
+
if (import.meta.main) {
|
|
107
|
+
await main();
|
|
108
|
+
}
|
package/src/cli/serve.ts
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* ndomo serve — start the HTTP server.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* bun run src/cli/serve.ts [options]
|
|
7
|
+
*
|
|
8
|
+
* Options:
|
|
9
|
+
* --port <n> Port number (default: from config, 4097)
|
|
10
|
+
* --no-auth Disable HTTP basic auth requirement
|
|
11
|
+
* --cors <origins> Comma-separated allowed origins (default: from config, "*")
|
|
12
|
+
* --force Start even when NDOMO_HTTP_ENABLED is not "true"
|
|
13
|
+
*
|
|
14
|
+
* Examples:
|
|
15
|
+
* bun run src/cli/serve.ts
|
|
16
|
+
* bun run src/cli/serve.ts --port 8080 --no-auth
|
|
17
|
+
* bun run src/cli/serve.ts --cors "https://app.example.com,https://admin.example.com"
|
|
18
|
+
* bun run src/cli/serve.ts --force --port 4098
|
|
19
|
+
*
|
|
20
|
+
* Graceful shutdown on SIGINT/SIGTERM.
|
|
21
|
+
* Exit codes: 0 = clean shutdown, 1 = startup failure.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { Database } from "bun:sqlite";
|
|
25
|
+
import { existsSync } from "node:fs";
|
|
26
|
+
import { join } from "node:path";
|
|
27
|
+
import type { HttpConfig } from "../config/schema.ts";
|
|
28
|
+
import { loadHttpConfig } from "../config/schema.ts";
|
|
29
|
+
import { runMigrations } from "../db/migrations.ts";
|
|
30
|
+
import { type HttpServerHandle, startHttpServer } from "../http/server.ts";
|
|
31
|
+
|
|
32
|
+
const NDOMO_DIR = ".ndomo";
|
|
33
|
+
const DB_FILE = "state.db";
|
|
34
|
+
|
|
35
|
+
/** Resolve DB path — same logic as src/db/client.ts. */
|
|
36
|
+
function resolveDbPath(): string | null {
|
|
37
|
+
const cwdPath = join(process.cwd(), NDOMO_DIR, DB_FILE);
|
|
38
|
+
if (existsSync(cwdPath)) return cwdPath;
|
|
39
|
+
|
|
40
|
+
let dir = process.cwd();
|
|
41
|
+
for (let i = 0; i < 5; i++) {
|
|
42
|
+
const parent = join(dir, "..");
|
|
43
|
+
if (parent === dir) break;
|
|
44
|
+
dir = parent;
|
|
45
|
+
const candidate = join(dir, NDOMO_DIR, DB_FILE);
|
|
46
|
+
if (existsSync(candidate)) return candidate;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Parse CLI args into a partial config override. */
|
|
53
|
+
function parseArgs(args: string[]): {
|
|
54
|
+
port?: number;
|
|
55
|
+
authRequired?: boolean;
|
|
56
|
+
corsOrigins?: string[];
|
|
57
|
+
force: boolean;
|
|
58
|
+
} {
|
|
59
|
+
const result: { port?: number; authRequired?: boolean; corsOrigins?: string[]; force: boolean } =
|
|
60
|
+
{
|
|
61
|
+
force: false,
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
for (let i = 0; i < args.length; i++) {
|
|
65
|
+
const arg = args[i];
|
|
66
|
+
if (arg === "--port" && i + 1 < args.length) {
|
|
67
|
+
const portStr = args[++i];
|
|
68
|
+
if (!portStr) {
|
|
69
|
+
console.error("error: --port requires a value");
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
const port = Number(portStr);
|
|
73
|
+
if (Number.isNaN(port) || port < 1 || port > 65535) {
|
|
74
|
+
console.error(`error: invalid port "${portStr}". Must be 1-65535.`);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
result.port = port;
|
|
78
|
+
} else if (arg === "--no-auth") {
|
|
79
|
+
result.authRequired = false;
|
|
80
|
+
} else if (arg === "--cors" && i + 1 < args.length) {
|
|
81
|
+
const corsStr = args[++i];
|
|
82
|
+
if (!corsStr) {
|
|
83
|
+
console.error("error: --cors requires a value");
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
result.corsOrigins = corsStr.split(",").map((s) => s.trim());
|
|
87
|
+
} else if (arg === "--force") {
|
|
88
|
+
result.force = true;
|
|
89
|
+
} else if (arg === "--help" || arg === "-h") {
|
|
90
|
+
console.log(`ndomo serve — start the HTTP server
|
|
91
|
+
|
|
92
|
+
Options:
|
|
93
|
+
--port <n> Port number (default: from config, 4097)
|
|
94
|
+
--no-auth Disable HTTP basic auth requirement
|
|
95
|
+
--cors <origins> Comma-separated allowed origins (default: from config, "*")
|
|
96
|
+
--force Start even when NDOMO_HTTP_ENABLED is not "true"
|
|
97
|
+
--help, -h Show this help`);
|
|
98
|
+
process.exit(0);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return result;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Print startup banner. */
|
|
106
|
+
function printBanner(config: HttpConfig, dbPath: string): void {
|
|
107
|
+
console.log(`
|
|
108
|
+
┌─────────────────────────────────────┐
|
|
109
|
+
│ ndomo HTTP server │
|
|
110
|
+
├─────────────────────────────────────┤
|
|
111
|
+
│ port: ${String(config.port).padEnd(25)}│
|
|
112
|
+
│ auth: ${(config.auth.required ? "enabled" : "disabled").padEnd(25)}│
|
|
113
|
+
│ cors: ${config.cors.origins.join(", ").padEnd(25).slice(0, 25)}│
|
|
114
|
+
│ db: ${dbPath.slice(-25).padEnd(25)}│
|
|
115
|
+
│ pid: ${String(process.pid).padEnd(25)}│
|
|
116
|
+
└─────────────────────────────────────┘`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Main entry point. */
|
|
120
|
+
export async function runServe(args: string[]): Promise<void> {
|
|
121
|
+
const opts = parseArgs(args);
|
|
122
|
+
|
|
123
|
+
// Resolve DB
|
|
124
|
+
const dbPath = resolveDbPath();
|
|
125
|
+
if (!dbPath) {
|
|
126
|
+
console.error("error: .ndomo/state.db not found — run from project root or parent dir");
|
|
127
|
+
process.exit(1);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Open DB
|
|
131
|
+
const db = new Database(dbPath);
|
|
132
|
+
db.exec("PRAGMA foreign_keys = ON");
|
|
133
|
+
runMigrations(db);
|
|
134
|
+
|
|
135
|
+
// Load config + apply CLI overrides
|
|
136
|
+
const config = loadHttpConfig();
|
|
137
|
+
|
|
138
|
+
if (opts.port !== undefined) config.port = opts.port;
|
|
139
|
+
if (opts.authRequired !== undefined) config.auth.required = opts.authRequired;
|
|
140
|
+
if (opts.corsOrigins !== undefined) config.cors.origins = opts.corsOrigins;
|
|
141
|
+
|
|
142
|
+
// Feature gate: require NDOMO_HTTP_ENABLED=true unless --force
|
|
143
|
+
if (!config.enabled && !opts.force) {
|
|
144
|
+
console.error(
|
|
145
|
+
"error: HTTP server is disabled (NDOMO_HTTP_ENABLED is not 'true'). Use --force to override.",
|
|
146
|
+
);
|
|
147
|
+
db.close();
|
|
148
|
+
process.exit(1);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
printBanner(config, dbPath);
|
|
152
|
+
|
|
153
|
+
// Start server
|
|
154
|
+
let serverHandle: HttpServerHandle;
|
|
155
|
+
try {
|
|
156
|
+
serverHandle = await startHttpServer({ db, httpConfig: config });
|
|
157
|
+
} catch (err) {
|
|
158
|
+
console.error(
|
|
159
|
+
`error: failed to start HTTP server: ${err instanceof Error ? err.message : String(err)}`,
|
|
160
|
+
);
|
|
161
|
+
db.close();
|
|
162
|
+
process.exit(1);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
console.log(`✓ HTTP server listening on port ${serverHandle.port}`);
|
|
166
|
+
|
|
167
|
+
// Graceful shutdown
|
|
168
|
+
let shuttingDown = false;
|
|
169
|
+
const shutdown = async (signal: string) => {
|
|
170
|
+
if (shuttingDown) return;
|
|
171
|
+
shuttingDown = true;
|
|
172
|
+
console.log(`\n${signal} received — shutting down...`);
|
|
173
|
+
try {
|
|
174
|
+
await serverHandle.stop();
|
|
175
|
+
db.close();
|
|
176
|
+
console.log("✓ Server stopped cleanly.");
|
|
177
|
+
process.exit(0);
|
|
178
|
+
} catch (err) {
|
|
179
|
+
console.error(`error during shutdown: ${err instanceof Error ? err.message : String(err)}`);
|
|
180
|
+
db.close();
|
|
181
|
+
process.exit(1);
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
186
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Direct execution
|
|
190
|
+
if (import.meta.main) {
|
|
191
|
+
runServe(process.argv.slice(2));
|
|
192
|
+
}
|