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
|
@@ -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,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,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,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,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,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;
|