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,129 @@
1
+ import { blue, bold, gray, green, red } from "./colors.js";
2
+ import { computeArchDiff } from "../engine/diff.js";
3
+ export function runDiffPlain(baseBranch) {
4
+ if (!baseBranch) {
5
+ // eslint-disable-next-line no-console
6
+ console.error("Missing base branch. Usage: tack diff <base-branch>");
7
+ return false;
8
+ }
9
+ let diff;
10
+ try {
11
+ diff = computeArchDiff(baseBranch);
12
+ }
13
+ catch (err) {
14
+ const message = err instanceof Error ? err.message : String(err);
15
+ // eslint-disable-next-line no-console
16
+ console.error(`✗ ${message}`);
17
+ return false;
18
+ }
19
+ const header = `${bold("Architecture diff")} ${gray(`(base: ${baseBranch}, head: ${diff.headRef})`)}`;
20
+ // eslint-disable-next-line no-console
21
+ console.log(header);
22
+ // eslint-disable-next-line no-console
23
+ console.log("");
24
+ if (diff.warnings.length > 0) {
25
+ // eslint-disable-next-line no-console
26
+ console.log(red("Warnings:"));
27
+ for (const w of diff.warnings) {
28
+ // eslint-disable-next-line no-console
29
+ console.log(` - ${w}`);
30
+ }
31
+ // eslint-disable-next-line no-console
32
+ console.log("");
33
+ }
34
+ // Systems
35
+ // eslint-disable-next-line no-console
36
+ console.log(bold("Systems:"));
37
+ if (!diff.systems.available) {
38
+ // eslint-disable-next-line no-console
39
+ console.log(gray(" (systems diff unavailable; see warnings above)"));
40
+ }
41
+ else if (diff.systems.added.length === 0 &&
42
+ diff.systems.removed.length === 0 &&
43
+ diff.systems.changed.length === 0) {
44
+ // eslint-disable-next-line no-console
45
+ console.log(green(" No system-level changes detected."));
46
+ }
47
+ else {
48
+ if (diff.systems.added.length > 0) {
49
+ // eslint-disable-next-line no-console
50
+ console.log(green(" Systems added:"));
51
+ for (const s of diff.systems.added) {
52
+ const detail = s.detail ? `: ${s.detail}` : "";
53
+ // eslint-disable-next-line no-console
54
+ console.log(` + ${s.id}${detail}`);
55
+ }
56
+ }
57
+ if (diff.systems.removed.length > 0) {
58
+ // eslint-disable-next-line no-console
59
+ console.log(red(" Systems removed:"));
60
+ for (const s of diff.systems.removed) {
61
+ const detail = s.detail ? `: ${s.detail}` : "";
62
+ // eslint-disable-next-line no-console
63
+ console.log(` - ${s.id}${detail}`);
64
+ }
65
+ }
66
+ if (diff.systems.changed.length > 0) {
67
+ // eslint-disable-next-line no-console
68
+ console.log(blue(" Systems changed:"));
69
+ for (const change of diff.systems.changed) {
70
+ const before = change.before.detail ?? "unknown";
71
+ const after = change.after.detail ?? "unknown";
72
+ // eslint-disable-next-line no-console
73
+ console.log(` ~ ${change.id}: ${before} → ${after}`);
74
+ }
75
+ }
76
+ }
77
+ // eslint-disable-next-line no-console
78
+ console.log("");
79
+ // Drift
80
+ // eslint-disable-next-line no-console
81
+ console.log(bold("Drift:"));
82
+ if (!diff.drift.available) {
83
+ // eslint-disable-next-line no-console
84
+ console.log(gray(" (drift status diff unavailable; see warnings above)"));
85
+ }
86
+ else if (diff.drift.newlyUnresolved.length === 0 &&
87
+ diff.drift.resolved.length === 0) {
88
+ // eslint-disable-next-line no-console
89
+ console.log(green(" No drift status changes detected."));
90
+ }
91
+ else {
92
+ if (diff.drift.newlyUnresolved.length > 0) {
93
+ // eslint-disable-next-line no-console
94
+ console.log(red(" Newly unresolved drift items:"));
95
+ for (const item of diff.drift.newlyUnresolved) {
96
+ const key = item.system ?? item.risk ?? item.type;
97
+ // eslint-disable-next-line no-console
98
+ console.log(` + ${key}: ${item.signal}`);
99
+ }
100
+ }
101
+ if (diff.drift.resolved.length > 0) {
102
+ // eslint-disable-next-line no-console
103
+ console.log(green(" Resolved drift items:"));
104
+ for (const change of diff.drift.resolved) {
105
+ const before = change.before;
106
+ const key = before?.system ?? before?.risk ?? before?.type ?? change.id;
107
+ const finalStatus = change.after?.status ?? "resolved";
108
+ // eslint-disable-next-line no-console
109
+ console.log(` - ${key}: unresolved → ${finalStatus}${before?.signal ? ` (${before.signal})` : ""}`);
110
+ }
111
+ }
112
+ }
113
+ // eslint-disable-next-line no-console
114
+ console.log("");
115
+ // Decisions
116
+ // eslint-disable-next-line no-console
117
+ console.log(bold("Decisions added since base:"));
118
+ if (diff.decisions.newDecisions.length === 0) {
119
+ // eslint-disable-next-line no-console
120
+ console.log(gray(" (no new decisions)"));
121
+ }
122
+ else {
123
+ for (const d of diff.decisions.newDecisions) {
124
+ // eslint-disable-next-line no-console
125
+ console.log(` - [${d.date}] ${d.decision} — ${d.reasoning}`);
126
+ }
127
+ }
128
+ return true;
129
+ }
@@ -0,0 +1 @@
1
+ export declare function printHandoffPlain(markdownPath: string, jsonPath: string, generatedAt: string): void;
@@ -0,0 +1,9 @@
1
+ import { blue, bold, green, gray } from "./colors.js";
2
+ export function printHandoffPlain(markdownPath, jsonPath, generatedAt) {
3
+ console.log(green("Handoff generated"));
4
+ console.log(`${gray("Time:")} ${blue(generatedAt)}`);
5
+ console.log(`${bold("Markdown:")} ${markdownPath}`);
6
+ console.log(`${bold("JSON:")} ${jsonPath}`);
7
+ console.log("");
8
+ console.log("Give this to your agent: attach the .md file to your chat (or add it to context in Cursor). For structured use, give the .json or use tack://handoff/latest (MCP).");
9
+ }
@@ -0,0 +1 @@
1
+ export declare function runInitPlain(): boolean;
@@ -0,0 +1,44 @@
1
+ import * as path from "node:path";
2
+ import { runAllDetectors } from "../detectors/index.js";
3
+ import { ensureTackIntegrity, ensureContextTemplates, ensureTackDir, projectRoot, specExists, writeAudit, writeDrift, writeSpec, } from "../lib/files.js";
4
+ import { log } from "../lib/logger.js";
5
+ import { createAudit, createEmptySpec } from "../lib/signals.js";
6
+ import { getProjectName } from "../lib/project.js";
7
+ export function runInitPlain() {
8
+ if (specExists()) {
9
+ const { repaired } = ensureTackIntegrity();
10
+ if (repaired.length > 0) {
11
+ log({ event: "repair", files: repaired });
12
+ console.log(`✓ Repaired .tack integrity (${repaired.length} file(s))`);
13
+ console.log(`Files recreated: ${repaired.join(", ")}`);
14
+ return true;
15
+ }
16
+ console.error("⚠ .tack already initialized. Run 'tack status' instead.");
17
+ return true;
18
+ }
19
+ ensureTackDir();
20
+ ensureContextTemplates();
21
+ const { signals } = runAllDetectors();
22
+ const projectName = getProjectName() || path.basename(projectRoot()) || "my-project";
23
+ const spec = createEmptySpec(projectName);
24
+ const inferredAllowed = Array.from(new Set(signals.filter((s) => s.category === "system" || s.category === "scope").map((s) => s.id)));
25
+ spec.allowed_systems = inferredAllowed;
26
+ writeSpec(spec);
27
+ writeAudit(createAudit(signals));
28
+ writeDrift({ items: [] });
29
+ log({
30
+ event: "init",
31
+ spec_seeded: true,
32
+ systems_detected: signals.filter((s) => s.category === "system").length,
33
+ });
34
+ console.log("✓ Initialized /.tack/");
35
+ console.log(`Project: ${projectName}`);
36
+ if (inferredAllowed.length > 0) {
37
+ console.log(`Allowed systems (seeded): ${inferredAllowed.join(", ")}`);
38
+ }
39
+ else {
40
+ console.log("Allowed systems (seeded): none detected");
41
+ }
42
+ console.log('Run "tack status" for a scan or "tack watch" for live monitoring.');
43
+ return true;
44
+ }
@@ -0,0 +1,5 @@
1
+ export declare function printNotes(opts?: {
2
+ limit?: number;
3
+ type?: string;
4
+ }): void;
5
+ export declare function addNotePlain(type: string, message: string, actor?: string): boolean;
@@ -0,0 +1,49 @@
1
+ import { AGENT_NOTE_TYPES } from "../lib/signals.js";
2
+ import { readNotes, addNote, formatRelativeTime } from "../lib/notes.js";
3
+ function isValidNoteType(value) {
4
+ return AGENT_NOTE_TYPES.includes(value);
5
+ }
6
+ export function printNotes(opts) {
7
+ const limit = opts?.limit;
8
+ const rawType = opts?.type;
9
+ let typeFilter;
10
+ if (typeof rawType === "string") {
11
+ if (!isValidNoteType(rawType)) {
12
+ // eslint-disable-next-line no-console
13
+ console.error(`Unknown note type: "${rawType}". Allowed types: ${AGENT_NOTE_TYPES.join(", ")}.`);
14
+ return;
15
+ }
16
+ typeFilter = rawType;
17
+ }
18
+ const notes = readNotes({ limit, type: typeFilter });
19
+ if (!notes.length) {
20
+ // eslint-disable-next-line no-console
21
+ console.log("No agent notes recorded.");
22
+ return;
23
+ }
24
+ for (const note of notes) {
25
+ const ago = formatRelativeTime(note.ts);
26
+ const actor = note.actor || "unknown";
27
+ // eslint-disable-next-line no-console
28
+ console.log(`[${note.type}] ${ago} — ${note.message} (${actor})`);
29
+ }
30
+ }
31
+ export function addNotePlain(type, message, actor) {
32
+ const normalizedActor = actor ?? "user";
33
+ if (!isValidNoteType(type)) {
34
+ // eslint-disable-next-line no-console
35
+ console.error(`Unknown note type: "${type}". Allowed types: ${AGENT_NOTE_TYPES.join(", ")}.`);
36
+ return false;
37
+ }
38
+ const ok = addNote({
39
+ type,
40
+ message,
41
+ actor: normalizedActor,
42
+ related_files: undefined,
43
+ });
44
+ if (!ok) {
45
+ // eslint-disable-next-line no-console
46
+ console.error("Failed to add note. See _logs.ndjson for details.");
47
+ }
48
+ return ok;
49
+ }
@@ -0,0 +1,2 @@
1
+ import type { ProjectStatus } from "../lib/signals.js";
2
+ export declare function printStatusPlain(status: ProjectStatus): void;
@@ -0,0 +1,13 @@
1
+ import { blue, bold, gray, green, red } from "./colors.js";
2
+ export function printStatusPlain(status) {
3
+ const healthy = status.health === "aligned";
4
+ console.log(`${bold("Project:")} ${status.name}`);
5
+ console.log(`${bold("Health:")} ${healthy ? green(status.health) : red(status.health)}`);
6
+ console.log(`${bold("Drift:")} ${status.driftCount > 0 ? red(`${status.driftCount} item(s)`) : green("0 item(s)")}`);
7
+ if (status.driftItems.length) {
8
+ for (const item of status.driftItems) {
9
+ console.log(` - ${red(item.system)}: ${item.message}`);
10
+ }
11
+ }
12
+ console.log(`${gray("Last scan:")} ${blue(status.lastScan ?? "never")}`);
13
+ }
@@ -0,0 +1 @@
1
+ export declare function runWatchPlain(): Promise<void>;
@@ -0,0 +1,78 @@
1
+ import chokidar from "chokidar";
2
+ import * as path from "node:path";
3
+ import { runStatusScan } from "../engine/status.js";
4
+ import { blue, gray, green, red } from "./colors.js";
5
+ const IGNORE_PATTERNS = [
6
+ "**/node_modules/**",
7
+ "**/.git/**",
8
+ "**/.tack/**",
9
+ "**/dist/**",
10
+ "**/build/**",
11
+ "**/.next/**",
12
+ "**/.cache/**",
13
+ "**/.svelte-kit/**",
14
+ "**/coverage/**",
15
+ "**/venv/**",
16
+ "**/.venv/**",
17
+ "**/env/**",
18
+ "**/site-packages/**",
19
+ ];
20
+ function printSnapshot(reason) {
21
+ const result = runStatusScan();
22
+ if (!result) {
23
+ console.error("No spec.yaml found. Run 'tack init' first.");
24
+ return false;
25
+ }
26
+ const ts = new Date().toISOString();
27
+ const healthy = result.status.health === "aligned";
28
+ console.log(`${blue(`[${ts}]`)} ${reason} :: health=${healthy ? green("aligned") : red("drift")} drift=${result.status.driftCount > 0 ? red(String(result.status.driftCount)) : green("0")}`);
29
+ for (const item of result.status.driftItems.slice(0, 5)) {
30
+ console.log(` - ${red(item.system)}: ${item.message}`);
31
+ }
32
+ if (result.status.driftItems.length > 5) {
33
+ console.log(` - ${gray(`...and ${result.status.driftItems.length - 5} more`)}`);
34
+ }
35
+ return true;
36
+ }
37
+ export async function runWatchPlain() {
38
+ const ok = printSnapshot("initial");
39
+ if (!ok)
40
+ return;
41
+ console.log(`${gray("Watching for changes (plain mode). Press Ctrl+C to stop.")}`);
42
+ const watcher = chokidar.watch(".", {
43
+ ignored: IGNORE_PATTERNS,
44
+ persistent: true,
45
+ ignoreInitial: true,
46
+ awaitWriteFinish: {
47
+ stabilityThreshold: 200,
48
+ pollInterval: 50,
49
+ },
50
+ });
51
+ let debounceTimer = null;
52
+ const shutdown = async () => {
53
+ if (debounceTimer) {
54
+ clearTimeout(debounceTimer);
55
+ debounceTimer = null;
56
+ }
57
+ await watcher.close();
58
+ console.log(gray("Stopped watch mode."));
59
+ };
60
+ watcher.on("all", (event, filepath) => {
61
+ if (filepath.includes(`${path.sep}.tack${path.sep}`))
62
+ return;
63
+ if (filepath.startsWith(".tack/") || filepath.startsWith(".tack\\"))
64
+ return;
65
+ if (debounceTimer)
66
+ clearTimeout(debounceTimer);
67
+ debounceTimer = setTimeout(() => {
68
+ printSnapshot(`${event} ${filepath}`);
69
+ }, 300);
70
+ });
71
+ await new Promise((resolve) => {
72
+ const onSignal = () => {
73
+ void shutdown().then(resolve);
74
+ };
75
+ process.once("SIGINT", onSignal);
76
+ process.once("SIGTERM", onSignal);
77
+ });
78
+ }
@@ -0,0 +1,5 @@
1
+ type Props = {
2
+ systemId: string;
3
+ };
4
+ export declare function CleanupPlan({ systemId }: Props): import("react/jsx-runtime").JSX.Element;
5
+ export {};
@@ -0,0 +1,8 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { useMemo } from "react";
3
+ import { Text, Box } from "ink";
4
+ import { generateCleanupPlan } from "../engine/cleanup.js";
5
+ export function CleanupPlan({ systemId }) {
6
+ const plan = useMemo(() => generateCleanupPlan(systemId), [systemId]);
7
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { bold: true, children: ["Cleanup plan for: ", systemId] }), plan.packagesToRemove.length > 0 && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { bold: true, children: "Packages to remove:" }), plan.packagesToRemove.map((pkg) => (_jsxs(Text, { children: [" bun remove ", pkg] }, pkg)))] })), plan.configFilesToCheck.length > 0 && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { bold: true, children: "Config files to check:" }), plan.configFilesToCheck.map((f) => (_jsxs(Text, { children: [" ", f] }, f)))] })), plan.filesToReview.length > 0 && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { bold: true, children: ["Files referencing ", systemId, ":"] }), plan.filesToReview.slice(0, 15).map((m, i) => (_jsxs(Text, { children: [" ", _jsxs(Text, { dimColor: true, children: [m.file, ":", m.line] }), " ", m.content.slice(0, 80)] }, `${m.file}-${m.line}-${i}`))), plan.filesToReview.length > 15 && _jsxs(Text, { dimColor: true, children: [" ", "...and ", plan.filesToReview.length - 15, " more"] })] })), plan.packagesToRemove.length === 0 && plan.filesToReview.length === 0 && plan.configFilesToCheck.length === 0 && (_jsx(Text, { dimColor: true, children: "No actionable cleanup items found." }))] }));
8
+ }
@@ -0,0 +1,6 @@
1
+ import type { Signal } from "../lib/signals.js";
2
+ type Props = {
3
+ onComplete: (signals: Signal[]) => void;
4
+ };
5
+ export declare function DetectorSweep({ onComplete }: Props): import("react/jsx-runtime").JSX.Element;
6
+ export {};
@@ -0,0 +1,54 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState, useEffect } from "react";
3
+ import { Text, Box } from "ink";
4
+ import Spinner from "ink-spinner";
5
+ import { PRIMARY_DETECTORS } from "../detectors/index.js";
6
+ import { detectDuplicates } from "../detectors/duplicates.js";
7
+ export function DetectorSweep({ onComplete }) {
8
+ const [detectors, setDetectors] = useState(PRIMARY_DETECTORS.map((d) => ({
9
+ name: d.name,
10
+ displayName: d.displayName,
11
+ status: "pending",
12
+ })));
13
+ useEffect(() => {
14
+ async function runSweep() {
15
+ const allSignals = [];
16
+ for (let i = 0; i < PRIMARY_DETECTORS.length; i += 1) {
17
+ const detector = PRIMARY_DETECTORS[i];
18
+ setDetectors((prev) => prev.map((d, idx) => (idx === i ? { ...d, status: "running" } : d)));
19
+ await new Promise((r) => setTimeout(r, 80));
20
+ const result = detector.run();
21
+ allSignals.push(...result.signals);
22
+ let summary = "none";
23
+ if (result.signals.length > 0) {
24
+ summary = result.signals.map((s) => s.detail ?? s.id).join(" + ");
25
+ }
26
+ setDetectors((prev) => prev.map((d, idx) => idx === i
27
+ ? {
28
+ ...d,
29
+ status: "done",
30
+ result,
31
+ summary,
32
+ }
33
+ : d));
34
+ }
35
+ const dupeResult = detectDuplicates(allSignals);
36
+ allSignals.push(...dupeResult.signals);
37
+ if (dupeResult.signals.length > 0) {
38
+ setDetectors((prev) => [
39
+ ...prev,
40
+ {
41
+ name: "duplicates",
42
+ displayName: "Checking for duplicate systems",
43
+ status: "warning",
44
+ result: dupeResult,
45
+ summary: dupeResult.signals.map((s) => s.detail ?? s.id).join(", "),
46
+ },
47
+ ]);
48
+ }
49
+ onComplete(allSignals);
50
+ }
51
+ void runSweep();
52
+ }, [onComplete]);
53
+ return (_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { bold: true, children: "Scanning project..." }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: detectors.map((d) => (_jsxs(Box, { children: [_jsxs(Box, { width: 2, children: [d.status === "running" && _jsx(Spinner, { type: "dots" }), d.status === "done" && _jsx(Text, { color: "green", children: "\u2713" }), d.status === "warning" && _jsx(Text, { color: "yellow", children: "\u26A0" }), d.status === "pending" && _jsx(Text, { dimColor: true, children: "\u25CB" })] }), _jsxs(Text, { children: [" ", d.displayName, d.summary && d.status !== "pending" && d.status !== "running" && _jsxs(Text, { dimColor: true, children: [": ", d.summary] })] })] }, d.name))) })] }));
54
+ }
@@ -0,0 +1,7 @@
1
+ import type { DriftItem } from "../lib/signals.js";
2
+ type Props = {
3
+ item: DriftItem;
4
+ onResolved: () => void;
5
+ };
6
+ export declare function DriftAlert({ item, onResolved }: Props): import("react/jsx-runtime").JSX.Element;
7
+ export {};
@@ -0,0 +1,105 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { useState } from "react";
3
+ import { Text, Box } from "ink";
4
+ import SelectInput from "ink-select-input";
5
+ import { resolveDriftItem } from "../engine/computeDrift.js";
6
+ import { readSpec, writeSpec } from "../lib/files.js";
7
+ import { CleanupPlan as CleanupPlanView } from "./CleanupPlan.js";
8
+ import { log } from "../lib/logger.js";
9
+ export function DriftAlert({ item, onResolved }) {
10
+ const [view, setView] = useState("options");
11
+ const [resolutionLabel, setResolutionLabel] = useState("");
12
+ const options = [
13
+ { label: "[a] Accept — add to allowed_systems", value: "accept" },
14
+ { label: "[d] Deny — add to forbidden_systems", value: "deny" },
15
+ { label: "[i] Investigate — show referencing files", value: "investigate" },
16
+ { label: "[g] Generate cleanup plan", value: "cleanup" },
17
+ { label: "[s] Skip for now", value: "skip" },
18
+ ];
19
+ function handleSelect(opt) {
20
+ switch (opt.value) {
21
+ case "accept": {
22
+ const spec = readSpec();
23
+ if (spec && item.system) {
24
+ let changed = false;
25
+ if (!spec.allowed_systems.includes(item.system)) {
26
+ spec.allowed_systems.push(item.system);
27
+ changed = true;
28
+ }
29
+ spec.forbidden_systems = spec.forbidden_systems.filter((s) => s !== item.system);
30
+ if (changed) {
31
+ log({
32
+ event: "spec:updated",
33
+ field: "allowed_systems",
34
+ diff: `added ${item.system}`,
35
+ });
36
+ }
37
+ writeSpec(spec);
38
+ }
39
+ resolveDriftItem(item.id, "accepted", "Accepted via tack watch");
40
+ log({
41
+ event: "decision",
42
+ decision: `Accepted drift item ${item.id}`,
43
+ reasoning: "Added detected system to allowed_systems from watch flow",
44
+ actor: "user",
45
+ });
46
+ setResolutionLabel("Accepted — spec updated");
47
+ setView("resolved");
48
+ onResolved();
49
+ break;
50
+ }
51
+ case "deny": {
52
+ const spec = readSpec();
53
+ if (spec && item.system) {
54
+ let changed = false;
55
+ if (!spec.forbidden_systems.includes(item.system)) {
56
+ spec.forbidden_systems.push(item.system);
57
+ changed = true;
58
+ }
59
+ spec.allowed_systems = spec.allowed_systems.filter((s) => s !== item.system);
60
+ if (changed) {
61
+ log({
62
+ event: "spec:updated",
63
+ field: "forbidden_systems",
64
+ diff: `added ${item.system}`,
65
+ });
66
+ }
67
+ writeSpec(spec);
68
+ }
69
+ resolveDriftItem(item.id, "rejected", "Rejected via tack watch");
70
+ log({
71
+ event: "decision",
72
+ decision: `Rejected drift item ${item.id}`,
73
+ reasoning: "Added detected system to forbidden_systems from watch flow",
74
+ actor: "user",
75
+ });
76
+ setResolutionLabel("Denied — spec updated");
77
+ setView("resolved");
78
+ onResolved();
79
+ break;
80
+ }
81
+ case "investigate":
82
+ case "cleanup": {
83
+ setView("cleanup");
84
+ break;
85
+ }
86
+ case "skip": {
87
+ resolveDriftItem(item.id, "skipped");
88
+ log({
89
+ event: "decision",
90
+ decision: `Skipped drift item ${item.id}`,
91
+ reasoning: "Deferred action from watch flow",
92
+ actor: "user",
93
+ });
94
+ setResolutionLabel("Skipped — will remind on next scan");
95
+ setView("resolved");
96
+ onResolved();
97
+ break;
98
+ }
99
+ default:
100
+ break;
101
+ }
102
+ }
103
+ const systemId = item.system ?? item.risk ?? "unknown";
104
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, marginY: 1, children: [_jsxs(Text, { bold: true, color: "yellow", children: ["\u26A0 Drift detected: ", systemId] }), _jsxs(Text, { children: [" Source: ", item.signal] }), _jsxs(Text, { children: [" Type: ", item.type.replace(/_/g, " ")] }), view === "options" && (_jsx(Box, { marginTop: 1, children: _jsx(SelectInput, { items: options, onSelect: handleSelect }) })), view === "cleanup" && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(CleanupPlanView, { systemId: systemId }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Press any key to return to options..." }) })] })), view === "resolved" && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "green", children: ["\u2713 ", resolutionLabel] }) }))] }));
105
+ }
@@ -0,0 +1 @@
1
+ export declare function Handoff(): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,37 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import React, { useState } from "react";
3
+ import { Box, Text, useApp } from "ink";
4
+ import { generateHandoff } from "../engine/handoff.js";
5
+ import { log } from "../lib/logger.js";
6
+ export function Handoff() {
7
+ const { exit } = useApp();
8
+ const [error, setError] = useState(null);
9
+ const [result, setResult] = useState(null);
10
+ React.useEffect(() => {
11
+ try {
12
+ const generated = generateHandoff();
13
+ setResult({
14
+ markdownPath: generated.markdownPath,
15
+ jsonPath: generated.jsonPath,
16
+ generatedAt: generated.report.generated_at,
17
+ });
18
+ log({
19
+ event: "handoff",
20
+ markdown_path: generated.markdownPath,
21
+ json_path: generated.jsonPath,
22
+ });
23
+ setTimeout(() => exit(), 100);
24
+ }
25
+ catch (err) {
26
+ setError(err instanceof Error ? err.message : "Failed to generate handoff");
27
+ setTimeout(() => exit(), 500);
28
+ }
29
+ }, [exit]);
30
+ if (error) {
31
+ return _jsxs(Text, { color: "red", children: ["\u2717 ", error] });
32
+ }
33
+ if (!result) {
34
+ return _jsx(Text, { children: "Generating handoff..." });
35
+ }
36
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "green", children: "\u2713 Handoff generated" }), _jsxs(Text, { children: [" Time: ", result.generatedAt] }), _jsxs(Text, { children: [" Markdown: ", result.markdownPath] }), _jsxs(Text, { children: [" JSON: ", result.jsonPath] }), _jsx(Text, { dimColor: true, children: " Give to your agent: .md for chat/context, .json or tack://handoff/latest for structured (MCP)." })] }));
37
+ }
@@ -0,0 +1 @@
1
+ export declare function Init(): import("react/jsx-runtime").JSX.Element;