tack-cli 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/LICENSE +21 -0
- package/README.md +232 -0
- package/dist/App.d.ts +5 -0
- package/dist/App.js +17 -0
- package/dist/detectors/admin.d.ts +2 -0
- package/dist/detectors/admin.js +33 -0
- package/dist/detectors/auth.d.ts +2 -0
- package/dist/detectors/auth.js +86 -0
- package/dist/detectors/database.d.ts +2 -0
- package/dist/detectors/database.js +96 -0
- package/dist/detectors/duplicates.d.ts +2 -0
- package/dist/detectors/duplicates.js +23 -0
- package/dist/detectors/exports.d.ts +2 -0
- package/dist/detectors/exports.js +30 -0
- package/dist/detectors/framework.d.ts +2 -0
- package/dist/detectors/framework.js +71 -0
- package/dist/detectors/index.d.ts +12 -0
- package/dist/detectors/index.js +128 -0
- package/dist/detectors/jobs.d.ts +2 -0
- package/dist/detectors/jobs.js +62 -0
- package/dist/detectors/multiuser.d.ts +2 -0
- package/dist/detectors/multiuser.js +55 -0
- package/dist/detectors/payments.d.ts +2 -0
- package/dist/detectors/payments.js +49 -0
- package/dist/detectors/rules/auth.yaml +24 -0
- package/dist/detectors/rules/database.yaml +27 -0
- package/dist/detectors/rules/exports.yaml +28 -0
- package/dist/detectors/rules/framework.yaml +26 -0
- package/dist/detectors/rules/jobs.yaml +23 -0
- package/dist/detectors/rules/payments.yaml +22 -0
- package/dist/detectors/types.d.ts +2 -0
- package/dist/detectors/types.js +1 -0
- package/dist/detectors/yamlRunner.d.ts +31 -0
- package/dist/detectors/yamlRunner.js +128 -0
- package/dist/engine/cleanup.d.ts +12 -0
- package/dist/engine/cleanup.js +101 -0
- package/dist/engine/compaction.d.ts +5 -0
- package/dist/engine/compaction.js +44 -0
- package/dist/engine/compareSpec.d.ts +2 -0
- package/dist/engine/compareSpec.js +74 -0
- package/dist/engine/computeDrift.d.ts +6 -0
- package/dist/engine/computeDrift.js +133 -0
- package/dist/engine/contextPack.d.ts +4 -0
- package/dist/engine/contextPack.js +169 -0
- package/dist/engine/decisions.d.ts +4 -0
- package/dist/engine/decisions.js +21 -0
- package/dist/engine/diff.d.ts +46 -0
- package/dist/engine/diff.js +210 -0
- package/dist/engine/handoff.d.ts +7 -0
- package/dist/engine/handoff.js +469 -0
- package/dist/engine/status.d.ts +10 -0
- package/dist/engine/status.js +46 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +299 -0
- package/dist/lib/cli.d.ts +4 -0
- package/dist/lib/cli.js +8 -0
- package/dist/lib/files.d.ts +48 -0
- package/dist/lib/files.js +529 -0
- package/dist/lib/git.d.ts +9 -0
- package/dist/lib/git.js +96 -0
- package/dist/lib/logger.d.ts +3 -0
- package/dist/lib/logger.js +21 -0
- package/dist/lib/ndjson.d.ts +2 -0
- package/dist/lib/ndjson.js +45 -0
- package/dist/lib/notes.d.ts +8 -0
- package/dist/lib/notes.js +144 -0
- package/dist/lib/notify.d.ts +1 -0
- package/dist/lib/notify.js +14 -0
- package/dist/lib/project.d.ts +1 -0
- package/dist/lib/project.js +17 -0
- package/dist/lib/promptSafety.d.ts +1 -0
- package/dist/lib/promptSafety.js +20 -0
- package/dist/lib/signals.d.ts +279 -0
- package/dist/lib/signals.js +55 -0
- package/dist/lib/tty.d.ts +2 -0
- package/dist/lib/tty.js +10 -0
- package/dist/lib/validate.d.ts +9 -0
- package/dist/lib/validate.js +282 -0
- package/dist/lib/yaml.d.ts +4 -0
- package/dist/lib/yaml.js +26 -0
- package/dist/mcp.d.ts +1 -0
- package/dist/mcp.js +259 -0
- package/dist/plain/colors.d.ts +5 -0
- package/dist/plain/colors.js +16 -0
- package/dist/plain/diff.d.ts +1 -0
- package/dist/plain/diff.js +129 -0
- package/dist/plain/handoff.d.ts +1 -0
- package/dist/plain/handoff.js +9 -0
- package/dist/plain/init.d.ts +1 -0
- package/dist/plain/init.js +44 -0
- package/dist/plain/notes.d.ts +5 -0
- package/dist/plain/notes.js +49 -0
- package/dist/plain/status.d.ts +2 -0
- package/dist/plain/status.js +13 -0
- package/dist/plain/watch.d.ts +1 -0
- package/dist/plain/watch.js +78 -0
- package/dist/ui/CleanupPlan.d.ts +5 -0
- package/dist/ui/CleanupPlan.js +8 -0
- package/dist/ui/DetectorSweep.d.ts +6 -0
- package/dist/ui/DetectorSweep.js +54 -0
- package/dist/ui/DriftAlert.d.ts +7 -0
- package/dist/ui/DriftAlert.js +105 -0
- package/dist/ui/Handoff.d.ts +1 -0
- package/dist/ui/Handoff.js +37 -0
- package/dist/ui/Init.d.ts +1 -0
- package/dist/ui/Init.js +117 -0
- package/dist/ui/Logo.d.ts +1 -0
- package/dist/ui/Logo.js +13 -0
- package/dist/ui/SpecSummary.d.ts +8 -0
- package/dist/ui/SpecSummary.js +15 -0
- package/dist/ui/Status.d.ts +1 -0
- package/dist/ui/Status.js +38 -0
- package/dist/ui/Watch.d.ts +1 -0
- package/dist/ui/Watch.js +136 -0
- package/dist/yoga.wasm +0 -0
- package/package.json +50 -0
package/dist/ui/Init.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React, { useState } from "react";
|
|
3
|
+
import { Text, Box, useApp } from "ink";
|
|
4
|
+
import SelectInput from "ink-select-input";
|
|
5
|
+
import TextInput from "ink-text-input";
|
|
6
|
+
import { Logo } from "./Logo.js";
|
|
7
|
+
import { DetectorSweep } from "./DetectorSweep.js";
|
|
8
|
+
import { ensureTackIntegrity, ensureTackDir, ensureContextTemplates, specExists, writeSpec, writeAudit, writeDrift, } from "../lib/files.js";
|
|
9
|
+
import { createAudit, createEmptySpec } from "../lib/signals.js";
|
|
10
|
+
import { log } from "../lib/logger.js";
|
|
11
|
+
export function Init() {
|
|
12
|
+
const { exit } = useApp();
|
|
13
|
+
const [phase, setPhase] = useState("logo");
|
|
14
|
+
const [signals, setSignals] = useState([]);
|
|
15
|
+
const [spec, setSpec] = useState(createEmptySpec("my-project"));
|
|
16
|
+
const [systemsToClassify, setSystemsToClassify] = useState([]);
|
|
17
|
+
const [currentSystemIndex, setCurrentSystemIndex] = useState(0);
|
|
18
|
+
const [projectName, setProjectName] = useState("");
|
|
19
|
+
const [forbiddenInput, setForbiddenInput] = useState("");
|
|
20
|
+
React.useEffect(() => {
|
|
21
|
+
if (phase === "logo") {
|
|
22
|
+
setTimeout(() => setPhase("check"), 500);
|
|
23
|
+
}
|
|
24
|
+
}, [phase]);
|
|
25
|
+
React.useEffect(() => {
|
|
26
|
+
if (phase === "check") {
|
|
27
|
+
if (specExists()) {
|
|
28
|
+
const { repaired } = ensureTackIntegrity();
|
|
29
|
+
if (repaired.length > 0) {
|
|
30
|
+
log({ event: "repair", files: repaired });
|
|
31
|
+
// eslint-disable-next-line no-console
|
|
32
|
+
console.log(`\n✓ Repaired .tack integrity (${repaired.length} file(s)): ${repaired.join(", ")}\n`);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
// eslint-disable-next-line no-console
|
|
36
|
+
console.log("\n⚠ .tack already initialized. Run 'tack status' instead.\n");
|
|
37
|
+
}
|
|
38
|
+
exit();
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
ensureTackDir();
|
|
42
|
+
ensureContextTemplates();
|
|
43
|
+
setPhase("sweep");
|
|
44
|
+
}
|
|
45
|
+
}, [phase, exit]);
|
|
46
|
+
function handleSweepComplete(detectedSignals) {
|
|
47
|
+
setSignals(detectedSignals);
|
|
48
|
+
const systems = detectedSignals.filter((s) => s.category === "system" || s.category === "scope");
|
|
49
|
+
const unique = new Map();
|
|
50
|
+
for (const s of systems) {
|
|
51
|
+
if (!unique.has(s.id))
|
|
52
|
+
unique.set(s.id, s);
|
|
53
|
+
}
|
|
54
|
+
const toClassify = Array.from(unique.values());
|
|
55
|
+
if (toClassify.length === 0) {
|
|
56
|
+
setPhase("project_name");
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
setSystemsToClassify(toClassify);
|
|
60
|
+
setCurrentSystemIndex(0);
|
|
61
|
+
setPhase("classify");
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function handleClassify(opt) {
|
|
65
|
+
const current = systemsToClassify[currentSystemIndex];
|
|
66
|
+
setSpec((prev) => {
|
|
67
|
+
const next = { ...prev };
|
|
68
|
+
if (opt.value === "allowed") {
|
|
69
|
+
next.allowed_systems = [...prev.allowed_systems, current.id];
|
|
70
|
+
}
|
|
71
|
+
else if (opt.value === "forbidden") {
|
|
72
|
+
next.forbidden_systems = [...prev.forbidden_systems, current.id];
|
|
73
|
+
}
|
|
74
|
+
return next;
|
|
75
|
+
});
|
|
76
|
+
if (currentSystemIndex + 1 < systemsToClassify.length) {
|
|
77
|
+
setCurrentSystemIndex((i) => i + 1);
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
setPhase("add_forbidden");
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function handleForbiddenSubmit(value) {
|
|
84
|
+
if (value.trim()) {
|
|
85
|
+
const items = value
|
|
86
|
+
.split(",")
|
|
87
|
+
.map((s) => s.trim())
|
|
88
|
+
.filter(Boolean);
|
|
89
|
+
setSpec((prev) => ({
|
|
90
|
+
...prev,
|
|
91
|
+
forbidden_systems: [...prev.forbidden_systems, ...items],
|
|
92
|
+
}));
|
|
93
|
+
}
|
|
94
|
+
setPhase("project_name");
|
|
95
|
+
}
|
|
96
|
+
function handleProjectName(value) {
|
|
97
|
+
const name = value.trim() || "my-project";
|
|
98
|
+
const finalSpec = { ...spec, project: name };
|
|
99
|
+
setSpec(finalSpec);
|
|
100
|
+
writeSpec(finalSpec);
|
|
101
|
+
writeAudit(createAudit(signals));
|
|
102
|
+
writeDrift({ items: [] });
|
|
103
|
+
log({
|
|
104
|
+
event: "init",
|
|
105
|
+
spec_seeded: true,
|
|
106
|
+
systems_detected: signals.filter((s) => s.category === "system").length,
|
|
107
|
+
});
|
|
108
|
+
setPhase("done");
|
|
109
|
+
setTimeout(() => exit(), 1000);
|
|
110
|
+
}
|
|
111
|
+
const classifyOptions = [
|
|
112
|
+
{ label: "Allowed — I want this", value: "allowed" },
|
|
113
|
+
{ label: "Forbidden — I don't want this", value: "forbidden" },
|
|
114
|
+
{ label: "Skip — decide later", value: "skip" },
|
|
115
|
+
];
|
|
116
|
+
return (_jsxs(Box, { flexDirection: "column", children: [(phase === "logo" || phase === "check") && _jsx(Logo, {}), phase === "sweep" && (_jsxs(_Fragment, { children: [_jsx(Logo, {}), _jsx(DetectorSweep, { onComplete: handleSweepComplete })] })), phase === "classify" && systemsToClassify[currentSystemIndex] && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { bold: true, children: ["Classify detected system (", currentSystemIndex + 1, "/", systemsToClassify.length, "):"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: systemsToClassify[currentSystemIndex].id }), systemsToClassify[currentSystemIndex].detail && (_jsxs(Text, { dimColor: true, children: [" (", systemsToClassify[currentSystemIndex].detail, ")"] }))] }), _jsxs(Text, { dimColor: true, children: [" ", "Source: ", systemsToClassify[currentSystemIndex].source] }), _jsxs(Text, { dimColor: true, children: [" ", "Use \u2191/\u2193 to choose, Enter to confirm."] }), _jsx(Box, { marginTop: 1, children: _jsx(SelectInput, { items: classifyOptions, onSelect: handleClassify }) })] })), phase === "add_forbidden" && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: "Any systems you want to explicitly forbid? (comma-separated, or Enter to skip)" }), _jsx(Text, { dimColor: true, children: "e.g.: multi_tenant, admin_panel, background_jobs" }), _jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { children: [">", " "] }), _jsx(TextInput, { value: forbiddenInput, onChange: setForbiddenInput, onSubmit: handleForbiddenSubmit })] })] })), phase === "project_name" && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: "Project name:" }), _jsxs(Box, { children: [_jsxs(Text, { children: [">", " "] }), _jsx(TextInput, { value: projectName, onChange: setProjectName, onSubmit: handleProjectName, placeholder: "my-project" })] })] })), phase === "done" && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: "green", bold: true, children: "\u2713 Initialized /.tack/" }), _jsx(Text, { children: " spec.yaml \u2014 your architecture contract" }), _jsx(Text, { children: " _audit.yaml \u2014 detector sweep results" }), _jsx(Text, { children: " _drift.yaml \u2014 drift tracking (empty)" }), _jsx(Text, { children: " _logs.ndjson \u2014 event log" }), _jsx(Text, { children: " context.md/goals.md/assumptions.md/open_questions.md \u2014 context templates" }), _jsxs(Text, { dimColor: true, children: ["\n", "Run \"tack status\" for a scan or \"tack watch\" for live monitoring."] })] }))] }));
|
|
117
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function Logo(): import("react/jsx-runtime").JSX.Element;
|
package/dist/ui/Logo.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Text, Box } from "ink";
|
|
3
|
+
const LOGO = `
|
|
4
|
+
████████╗ █████╗ ██████╗██╗ ██╗
|
|
5
|
+
╚══██╔══╝██╔══██╗██╔════╝██║ ██╔╝
|
|
6
|
+
██║ ███████║██║ █████╔╝
|
|
7
|
+
██║ ██╔══██║██║ ██╔═██╗
|
|
8
|
+
██║ ██║ ██║╚██████╗██║ ██╗
|
|
9
|
+
╚═╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝
|
|
10
|
+
`;
|
|
11
|
+
export function Logo() {
|
|
12
|
+
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { color: "cyan", children: LOGO }), _jsx(Text, { dimColor: true, children: " Architecture drift guard \u2022 v0.1.0" })] }));
|
|
13
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Spec, SpecDiff, DriftState } from "../lib/signals.js";
|
|
2
|
+
type Props = {
|
|
3
|
+
spec: Spec;
|
|
4
|
+
diff: SpecDiff;
|
|
5
|
+
drift?: DriftState;
|
|
6
|
+
};
|
|
7
|
+
export declare function SpecSummary({ spec, diff, drift }: Props): import("react/jsx-runtime").JSX.Element;
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { Text, Box } from "ink";
|
|
3
|
+
export function SpecSummary({ spec, diff, drift }) {
|
|
4
|
+
const unresolvedCount = drift?.items.filter((i) => i.status === "unresolved").length ?? 0;
|
|
5
|
+
return (_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsxs(Text, { bold: true, children: ["tack status \u2014 ", spec.project] }), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { bold: true, underline: true, children: "SYSTEMS" }), diff.aligned
|
|
6
|
+
.filter((s) => s.category === "system")
|
|
7
|
+
.map((s) => (_jsxs(Text, { children: [" ", _jsx(Text, { color: "green", children: "\u2713" }), " ", s.id, ": ", s.detail ?? "detected", " ", _jsx(Text, { dimColor: true, children: "(allowed)" })] }, `${s.id}-${s.detail}`))), diff.violations
|
|
8
|
+
.filter((v) => v.type === "forbidden_system")
|
|
9
|
+
.map((v) => (_jsxs(Text, { children: [" ", _jsx(Text, { color: "red", children: "\u2717" }), " ", v.signal.id, " ", _jsxs(Text, { color: "red", children: ["(FORBIDDEN \u2014 ", v.signal.source, ")"] })] }, `${v.signal.id}-${v.signal.source}`))), diff.undeclared
|
|
10
|
+
.filter((s) => s.category === "system")
|
|
11
|
+
.map((s) => (_jsxs(Text, { children: [" ", _jsx(Text, { color: "yellow", children: "?" }), " ", s.id, ": ", s.detail ?? "detected", " ", _jsx(Text, { color: "yellow", children: "(undeclared)" })] }, `${s.id}-${s.source}`))), diff.missing.map((id) => (_jsxs(Text, { children: [" ", _jsx(Text, { dimColor: true, children: "-" }), " ", id, " ", _jsx(Text, { dimColor: true, children: "(allowed but not detected)" })] }, id)))] }), Object.keys(spec.constraints).length > 0 && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { bold: true, underline: true, children: "CONSTRAINTS" }), Object.entries(spec.constraints).map(([key, val]) => {
|
|
12
|
+
const mismatch = diff.violations.find((v) => v.type === "constraint_mismatch" && v.spec_rule.includes(key));
|
|
13
|
+
return (_jsxs(Text, { children: [" ", mismatch ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: "red", children: "\u2717" }), " ", key, ": expected ", val, ", found ", mismatch.signal.detail] })) : (_jsxs(_Fragment, { children: [_jsx(Text, { color: "green", children: "\u2713" }), " ", key, ": ", val] }))] }, key));
|
|
14
|
+
})] })), diff.risks.length > 0 && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { bold: true, underline: true, children: "RISKS" }), diff.risks.map((r) => (_jsxs(Text, { children: [" ", _jsx(Text, { color: "yellow", children: "\u26A0" }), " ", r.detail ?? r.id] }, r.id)))] })), unresolvedCount > 0 && (_jsxs(_Fragment, { children: [_jsx(Box, { marginTop: 1, children: _jsx(Text, { bold: true, underline: true, children: "DRIFT" }) }), _jsxs(Text, { children: [" ", unresolvedCount, " unresolved item(s) ", _jsx(Text, { dimColor: true, children: "(run tack watch to resolve)" })] })] })), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: ["Complexity: ", diff.aligned.filter((s) => s.category === "system").length, " system(s)"] }) })] }));
|
|
15
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function Status(): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React, { useState } from "react";
|
|
3
|
+
import { Text, Box, useApp } from "ink";
|
|
4
|
+
import { DetectorSweep } from "./DetectorSweep.js";
|
|
5
|
+
import { SpecSummary } from "./SpecSummary.js";
|
|
6
|
+
import { readSpec } from "../lib/files.js";
|
|
7
|
+
import { computeStatusFromSignals } from "../engine/status.js";
|
|
8
|
+
export function Status() {
|
|
9
|
+
const { exit } = useApp();
|
|
10
|
+
const [phase, setPhase] = useState("check");
|
|
11
|
+
const [error, setError] = useState("");
|
|
12
|
+
const [summaryData, setSummaryData] = useState(null);
|
|
13
|
+
React.useEffect(() => {
|
|
14
|
+
if (phase !== "check")
|
|
15
|
+
return;
|
|
16
|
+
const spec = readSpec();
|
|
17
|
+
if (!spec) {
|
|
18
|
+
setError("No spec.yaml found. Run 'tack init' first.");
|
|
19
|
+
setPhase("error");
|
|
20
|
+
setTimeout(() => exit(), 500);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
setPhase("sweep");
|
|
24
|
+
}, [phase, exit]);
|
|
25
|
+
function handleSweepComplete(signals) {
|
|
26
|
+
const computed = computeStatusFromSignals(signals);
|
|
27
|
+
if (!computed) {
|
|
28
|
+
setError("No spec.yaml found. Run 'tack init' first.");
|
|
29
|
+
setPhase("error");
|
|
30
|
+
setTimeout(() => exit(), 500);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
setSummaryData({ spec: computed.spec, diff: computed.diff, drift: computed.drift });
|
|
34
|
+
setPhase("summary");
|
|
35
|
+
setTimeout(() => exit(), 100);
|
|
36
|
+
}
|
|
37
|
+
return (_jsxs(Box, { flexDirection: "column", children: [phase === "sweep" && _jsx(DetectorSweep, { onComplete: handleSweepComplete }), phase === "summary" && summaryData && _jsx(SpecSummary, { spec: summaryData.spec, diff: summaryData.diff, drift: summaryData.drift }), phase === "error" && _jsxs(Text, { color: "red", children: ["\u2717 ", error] })] }));
|
|
38
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function Watch(): import("react/jsx-runtime").JSX.Element;
|
package/dist/ui/Watch.js
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useEffect, useRef } from "react";
|
|
3
|
+
import { Text, Box, Static, useApp, useInput } from "ink";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { Logo } from "./Logo.js";
|
|
6
|
+
import { DriftAlert } from "./DriftAlert.js";
|
|
7
|
+
import { readSpec, readDrift, writeAudit } from "../lib/files.js";
|
|
8
|
+
import { createAudit } from "../lib/signals.js";
|
|
9
|
+
import { runAllDetectors } from "../detectors/index.js";
|
|
10
|
+
import { compareSpec } from "../engine/compareSpec.js";
|
|
11
|
+
import { computeDrift } from "../engine/computeDrift.js";
|
|
12
|
+
import { notify } from "../lib/notify.js";
|
|
13
|
+
import { log } from "../lib/logger.js";
|
|
14
|
+
import chokidar from "chokidar";
|
|
15
|
+
const IGNORE_PATTERNS = [
|
|
16
|
+
"**/node_modules/**",
|
|
17
|
+
"**/.git/**",
|
|
18
|
+
"**/.tack/**",
|
|
19
|
+
"**/dist/**",
|
|
20
|
+
"**/build/**",
|
|
21
|
+
"**/.next/**",
|
|
22
|
+
"**/.cache/**",
|
|
23
|
+
"**/.svelte-kit/**",
|
|
24
|
+
"**/coverage/**",
|
|
25
|
+
"**/venv/**",
|
|
26
|
+
"**/.venv/**",
|
|
27
|
+
"**/env/**",
|
|
28
|
+
"**/site-packages/**",
|
|
29
|
+
];
|
|
30
|
+
export function Watch() {
|
|
31
|
+
const { exit } = useApp();
|
|
32
|
+
const [systemCount, setSystemCount] = useState(0);
|
|
33
|
+
const [driftCount, setDriftCount] = useState(0);
|
|
34
|
+
const [lastScan, setLastScan] = useState("never");
|
|
35
|
+
const [pendingAlerts, setPendingAlerts] = useState([]);
|
|
36
|
+
const [projectName, setProjectName] = useState("unknown");
|
|
37
|
+
const [history, setHistory] = useState([]);
|
|
38
|
+
const watcherRef = useRef(null);
|
|
39
|
+
const debounceTimer = useRef(null);
|
|
40
|
+
const historySeq = useRef(0);
|
|
41
|
+
function pushHistory(level, text) {
|
|
42
|
+
historySeq.current += 1;
|
|
43
|
+
setHistory((prev) => {
|
|
44
|
+
const next = [...prev, { id: historySeq.current, level, text }];
|
|
45
|
+
return next.slice(-40);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
function runScan(reason = "scan") {
|
|
49
|
+
const startedAt = Date.now();
|
|
50
|
+
const spec = readSpec();
|
|
51
|
+
if (!spec)
|
|
52
|
+
return;
|
|
53
|
+
setProjectName(spec.project);
|
|
54
|
+
const { signals } = runAllDetectors();
|
|
55
|
+
const audit = createAudit(signals);
|
|
56
|
+
writeAudit(audit);
|
|
57
|
+
const diff = compareSpec(signals, spec);
|
|
58
|
+
const { newItems, state } = computeDrift(diff);
|
|
59
|
+
const unresolvedCount = state.items.filter((i) => i.status === "unresolved").length;
|
|
60
|
+
setSystemCount(diff.aligned.filter((s) => s.category === "system").length);
|
|
61
|
+
setDriftCount(unresolvedCount);
|
|
62
|
+
setLastScan(new Date().toLocaleTimeString());
|
|
63
|
+
const scanTs = new Date().toLocaleTimeString();
|
|
64
|
+
if (unresolvedCount === 0) {
|
|
65
|
+
pushHistory("good", `[${scanTs}] ${reason}: scan clean (0 drift)`);
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
pushHistory("bad", `[${scanTs}] ${reason}: ${unresolvedCount} unresolved drift item(s)`);
|
|
69
|
+
}
|
|
70
|
+
log({
|
|
71
|
+
event: "scan",
|
|
72
|
+
systems_detected: signals.filter((s) => s.category === "system").length,
|
|
73
|
+
drift_items: unresolvedCount,
|
|
74
|
+
duration_ms: Date.now() - startedAt,
|
|
75
|
+
});
|
|
76
|
+
const alertable = newItems.filter((i) => i.type === "forbidden_system_detected" ||
|
|
77
|
+
i.type === "constraint_mismatch" ||
|
|
78
|
+
i.type === "risk" ||
|
|
79
|
+
i.type === "undeclared_system");
|
|
80
|
+
if (alertable.length > 0) {
|
|
81
|
+
for (const item of alertable) {
|
|
82
|
+
notify("⚠ Tack: Drift Detected", `${item.system ?? item.risk}: ${item.signal}`);
|
|
83
|
+
}
|
|
84
|
+
setPendingAlerts((prev) => [...prev, ...alertable]);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
const spec = readSpec();
|
|
89
|
+
if (!spec) {
|
|
90
|
+
// eslint-disable-next-line no-console
|
|
91
|
+
console.error("No spec.yaml found. Run 'tack init' first.");
|
|
92
|
+
exit();
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
runScan();
|
|
96
|
+
const watcher = chokidar.watch(".", {
|
|
97
|
+
ignored: IGNORE_PATTERNS,
|
|
98
|
+
persistent: true,
|
|
99
|
+
ignoreInitial: true,
|
|
100
|
+
awaitWriteFinish: {
|
|
101
|
+
stabilityThreshold: 200,
|
|
102
|
+
pollInterval: 50,
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
watcher.on("all", (event, filepath) => {
|
|
106
|
+
if (filepath.includes(`${path.sep}.tack${path.sep}`))
|
|
107
|
+
return;
|
|
108
|
+
if (filepath.startsWith(".tack/") || filepath.startsWith(".tack\\"))
|
|
109
|
+
return;
|
|
110
|
+
pushHistory("update", "Filesystem change detected. Running scan...");
|
|
111
|
+
if (debounceTimer.current)
|
|
112
|
+
clearTimeout(debounceTimer.current);
|
|
113
|
+
debounceTimer.current = setTimeout(() => {
|
|
114
|
+
runScan(`change (${event})`);
|
|
115
|
+
}, 300);
|
|
116
|
+
});
|
|
117
|
+
watcherRef.current = watcher;
|
|
118
|
+
return () => {
|
|
119
|
+
void watcher.close();
|
|
120
|
+
if (debounceTimer.current)
|
|
121
|
+
clearTimeout(debounceTimer.current);
|
|
122
|
+
};
|
|
123
|
+
}, [exit]);
|
|
124
|
+
function handleAlertResolved() {
|
|
125
|
+
setPendingAlerts((prev) => prev.slice(1));
|
|
126
|
+
const drift = readDrift();
|
|
127
|
+
setDriftCount(drift.items.filter((i) => i.status === "unresolved").length);
|
|
128
|
+
}
|
|
129
|
+
useInput((input) => {
|
|
130
|
+
if (input === "q") {
|
|
131
|
+
void watcherRef.current?.close();
|
|
132
|
+
exit();
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Logo, {}), _jsxs(Box, { borderStyle: "single", borderColor: "gray", paddingX: 1, justifyContent: "space-between", children: [_jsxs(Text, { children: [_jsx(Text, { bold: true, children: projectName }), " ", _jsxs(Text, { color: "green", children: [systemCount, " systems"] }), " ", driftCount > 0 ? _jsxs(Text, { color: "yellow", children: [driftCount, " drift"] }) : _jsx(Text, { color: "green", children: "0 drift" })] }), _jsxs(Text, { dimColor: true, children: ["Last scan: ", lastScan] })] }), pendingAlerts.length > 0 && pendingAlerts[0] && _jsx(DriftAlert, { item: pendingAlerts[0], onResolved: handleAlertResolved }), pendingAlerts.length === 0 && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Watching for changes... (q to quit)" }) })), _jsx(Box, { marginTop: 1, flexDirection: "column", children: _jsx(Static, { items: history, children: (item) => (_jsx(Text, { color: item.level === "good" ? "green" : item.level === "bad" ? "red" : "blue", children: item.text })) }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "\u2500\u2500\u2500 Run in another terminal \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }) }), _jsxs(Box, { flexDirection: "column", paddingLeft: 2, children: [_jsxs(Text, { children: [_jsx(Text, { color: "green", children: "tack status" }), _jsx(Text, { dimColor: true, children: " Project health snapshot" })] }), _jsxs(Text, { children: [_jsx(Text, { color: "green", children: "tack handoff" }), _jsx(Text, { dimColor: true, children: " Generate handoff for agents" })] }), _jsxs(Text, { children: [_jsx(Text, { color: "green", children: "tack check-in" }), _jsx(Text, { dimColor: true, children: " Morning/evening pulse" })] }), _jsxs(Text, { children: [_jsx(Text, { color: "green", children: "tack log" }), _jsx(Text, { dimColor: true, children: " View or append decisions" })] }), _jsxs(Text, { children: [_jsx(Text, { color: "green", children: "tack help" }), _jsx(Text, { dimColor: true, children: " All commands and options" })] })] })] }));
|
|
136
|
+
}
|
package/dist/yoga.wasm
ADDED
|
Binary file
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "tack-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Architecture drift guard. Declare your spec. Tack enforces it.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"files": [
|
|
7
|
+
"dist/",
|
|
8
|
+
"README.md",
|
|
9
|
+
"LICENSE"
|
|
10
|
+
],
|
|
11
|
+
"bin": {
|
|
12
|
+
"tack": "dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"dev": "npm run build && node dist/index.js watch --plain",
|
|
16
|
+
"dev:bun": "bun run src/index.tsx",
|
|
17
|
+
"build": "tsc && node scripts/postbuild.cjs",
|
|
18
|
+
"build:bun": "bun build src/index.tsx --outdir dist --target node && node scripts/postbuild.cjs",
|
|
19
|
+
"typecheck": "tsc --noEmit",
|
|
20
|
+
"prepublishOnly": "npm run build && npm pack --dry-run"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
24
|
+
"chokidar": "^3.6.0",
|
|
25
|
+
"glob": "^10.3.10",
|
|
26
|
+
"ink": "^4.4.1",
|
|
27
|
+
"ink-select-input": "^5.0.0",
|
|
28
|
+
"ink-spinner": "^5.0.0",
|
|
29
|
+
"ink-text-input": "^5.0.1",
|
|
30
|
+
"js-yaml": "^4.1.0",
|
|
31
|
+
"minimist": "^1.2.8",
|
|
32
|
+
"node-notifier": "^10.0.1",
|
|
33
|
+
"picocolors": "^1.1.1",
|
|
34
|
+
"react": "^18.2.0",
|
|
35
|
+
"tasuku": "^2.0.1",
|
|
36
|
+
"update-notifier": "^7.3.1",
|
|
37
|
+
"yoga-wasm-web": "~0.3.3"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/bun": "latest",
|
|
41
|
+
"@types/js-yaml": "^4.0.9",
|
|
42
|
+
"@types/minimist": "^1.2.5",
|
|
43
|
+
"@types/node-notifier": "^8.0.5",
|
|
44
|
+
"@types/react": "^18.2.0",
|
|
45
|
+
"@types/update-notifier": "^6.0.8",
|
|
46
|
+
"react-devtools-core": "^4.28.5",
|
|
47
|
+
"typescript": "^5.3.3"
|
|
48
|
+
},
|
|
49
|
+
"license": "MIT"
|
|
50
|
+
}
|