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.
Files changed (116) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +232 -0
  3. package/dist/App.d.ts +5 -0
  4. package/dist/App.js +17 -0
  5. package/dist/detectors/admin.d.ts +2 -0
  6. package/dist/detectors/admin.js +33 -0
  7. package/dist/detectors/auth.d.ts +2 -0
  8. package/dist/detectors/auth.js +86 -0
  9. package/dist/detectors/database.d.ts +2 -0
  10. package/dist/detectors/database.js +96 -0
  11. package/dist/detectors/duplicates.d.ts +2 -0
  12. package/dist/detectors/duplicates.js +23 -0
  13. package/dist/detectors/exports.d.ts +2 -0
  14. package/dist/detectors/exports.js +30 -0
  15. package/dist/detectors/framework.d.ts +2 -0
  16. package/dist/detectors/framework.js +71 -0
  17. package/dist/detectors/index.d.ts +12 -0
  18. package/dist/detectors/index.js +128 -0
  19. package/dist/detectors/jobs.d.ts +2 -0
  20. package/dist/detectors/jobs.js +62 -0
  21. package/dist/detectors/multiuser.d.ts +2 -0
  22. package/dist/detectors/multiuser.js +55 -0
  23. package/dist/detectors/payments.d.ts +2 -0
  24. package/dist/detectors/payments.js +49 -0
  25. package/dist/detectors/rules/auth.yaml +24 -0
  26. package/dist/detectors/rules/database.yaml +27 -0
  27. package/dist/detectors/rules/exports.yaml +28 -0
  28. package/dist/detectors/rules/framework.yaml +26 -0
  29. package/dist/detectors/rules/jobs.yaml +23 -0
  30. package/dist/detectors/rules/payments.yaml +22 -0
  31. package/dist/detectors/types.d.ts +2 -0
  32. package/dist/detectors/types.js +1 -0
  33. package/dist/detectors/yamlRunner.d.ts +31 -0
  34. package/dist/detectors/yamlRunner.js +128 -0
  35. package/dist/engine/cleanup.d.ts +12 -0
  36. package/dist/engine/cleanup.js +101 -0
  37. package/dist/engine/compaction.d.ts +5 -0
  38. package/dist/engine/compaction.js +44 -0
  39. package/dist/engine/compareSpec.d.ts +2 -0
  40. package/dist/engine/compareSpec.js +74 -0
  41. package/dist/engine/computeDrift.d.ts +6 -0
  42. package/dist/engine/computeDrift.js +133 -0
  43. package/dist/engine/contextPack.d.ts +4 -0
  44. package/dist/engine/contextPack.js +169 -0
  45. package/dist/engine/decisions.d.ts +4 -0
  46. package/dist/engine/decisions.js +21 -0
  47. package/dist/engine/diff.d.ts +46 -0
  48. package/dist/engine/diff.js +210 -0
  49. package/dist/engine/handoff.d.ts +7 -0
  50. package/dist/engine/handoff.js +469 -0
  51. package/dist/engine/status.d.ts +10 -0
  52. package/dist/engine/status.js +46 -0
  53. package/dist/index.d.ts +2 -0
  54. package/dist/index.js +299 -0
  55. package/dist/lib/cli.d.ts +4 -0
  56. package/dist/lib/cli.js +8 -0
  57. package/dist/lib/files.d.ts +48 -0
  58. package/dist/lib/files.js +529 -0
  59. package/dist/lib/git.d.ts +9 -0
  60. package/dist/lib/git.js +96 -0
  61. package/dist/lib/logger.d.ts +3 -0
  62. package/dist/lib/logger.js +21 -0
  63. package/dist/lib/ndjson.d.ts +2 -0
  64. package/dist/lib/ndjson.js +45 -0
  65. package/dist/lib/notes.d.ts +8 -0
  66. package/dist/lib/notes.js +144 -0
  67. package/dist/lib/notify.d.ts +1 -0
  68. package/dist/lib/notify.js +14 -0
  69. package/dist/lib/project.d.ts +1 -0
  70. package/dist/lib/project.js +17 -0
  71. package/dist/lib/promptSafety.d.ts +1 -0
  72. package/dist/lib/promptSafety.js +20 -0
  73. package/dist/lib/signals.d.ts +279 -0
  74. package/dist/lib/signals.js +55 -0
  75. package/dist/lib/tty.d.ts +2 -0
  76. package/dist/lib/tty.js +10 -0
  77. package/dist/lib/validate.d.ts +9 -0
  78. package/dist/lib/validate.js +282 -0
  79. package/dist/lib/yaml.d.ts +4 -0
  80. package/dist/lib/yaml.js +26 -0
  81. package/dist/mcp.d.ts +1 -0
  82. package/dist/mcp.js +259 -0
  83. package/dist/plain/colors.d.ts +5 -0
  84. package/dist/plain/colors.js +16 -0
  85. package/dist/plain/diff.d.ts +1 -0
  86. package/dist/plain/diff.js +129 -0
  87. package/dist/plain/handoff.d.ts +1 -0
  88. package/dist/plain/handoff.js +9 -0
  89. package/dist/plain/init.d.ts +1 -0
  90. package/dist/plain/init.js +44 -0
  91. package/dist/plain/notes.d.ts +5 -0
  92. package/dist/plain/notes.js +49 -0
  93. package/dist/plain/status.d.ts +2 -0
  94. package/dist/plain/status.js +13 -0
  95. package/dist/plain/watch.d.ts +1 -0
  96. package/dist/plain/watch.js +78 -0
  97. package/dist/ui/CleanupPlan.d.ts +5 -0
  98. package/dist/ui/CleanupPlan.js +8 -0
  99. package/dist/ui/DetectorSweep.d.ts +6 -0
  100. package/dist/ui/DetectorSweep.js +54 -0
  101. package/dist/ui/DriftAlert.d.ts +7 -0
  102. package/dist/ui/DriftAlert.js +105 -0
  103. package/dist/ui/Handoff.d.ts +1 -0
  104. package/dist/ui/Handoff.js +37 -0
  105. package/dist/ui/Init.d.ts +1 -0
  106. package/dist/ui/Init.js +117 -0
  107. package/dist/ui/Logo.d.ts +1 -0
  108. package/dist/ui/Logo.js +13 -0
  109. package/dist/ui/SpecSummary.d.ts +8 -0
  110. package/dist/ui/SpecSummary.js +15 -0
  111. package/dist/ui/Status.d.ts +1 -0
  112. package/dist/ui/Status.js +38 -0
  113. package/dist/ui/Watch.d.ts +1 -0
  114. package/dist/ui/Watch.js +136 -0
  115. package/dist/yoga.wasm +0 -0
  116. package/package.json +50 -0
@@ -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;
@@ -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;
@@ -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
+ }