@vreko/cli 3.0.1

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 (98) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +45 -0
  3. package/dist/CeremonyView-LQS7FTMK.js +134 -0
  4. package/dist/CeremonyView-LQS7FTMK.js.map +1 -0
  5. package/dist/InitApp-7K5DTYSW.js +1479 -0
  6. package/dist/InitApp-7K5DTYSW.js.map +1 -0
  7. package/dist/SkippedTestDetector-PJSKSOZR.js +7 -0
  8. package/dist/SkippedTestDetector-PJSKSOZR.js.map +1 -0
  9. package/dist/TuiApp-FX23XQBK.js +8 -0
  10. package/dist/TuiApp-FX23XQBK.js.map +1 -0
  11. package/dist/analysis-ABEO6RTN.js +8 -0
  12. package/dist/analysis-ABEO6RTN.js.map +1 -0
  13. package/dist/auth-XNBEBNPY.js +7669 -0
  14. package/dist/auth-XNBEBNPY.js.map +1 -0
  15. package/dist/ceremony-M7CXVBVA.js +45 -0
  16. package/dist/ceremony-M7CXVBVA.js.map +1 -0
  17. package/dist/chunk-A3QSZJPD.js +3147 -0
  18. package/dist/chunk-A3QSZJPD.js.map +1 -0
  19. package/dist/chunk-ASGZ5B6C.js +3969 -0
  20. package/dist/chunk-ASGZ5B6C.js.map +1 -0
  21. package/dist/chunk-DMXC2JTC.js +58 -0
  22. package/dist/chunk-DMXC2JTC.js.map +1 -0
  23. package/dist/chunk-EEBSK2IH.js +161 -0
  24. package/dist/chunk-EEBSK2IH.js.map +1 -0
  25. package/dist/chunk-EWOJGXRX.js +22 -0
  26. package/dist/chunk-EWOJGXRX.js.map +1 -0
  27. package/dist/chunk-F7GEJLP7.js +2389 -0
  28. package/dist/chunk-F7GEJLP7.js.map +1 -0
  29. package/dist/chunk-GOYL3F4T.js +605 -0
  30. package/dist/chunk-GOYL3F4T.js.map +1 -0
  31. package/dist/chunk-GRMRYWYS.js +17 -0
  32. package/dist/chunk-GRMRYWYS.js.map +1 -0
  33. package/dist/chunk-GSUGROXB.js +1951 -0
  34. package/dist/chunk-GSUGROXB.js.map +1 -0
  35. package/dist/chunk-H7773ONB.js +50 -0
  36. package/dist/chunk-H7773ONB.js.map +1 -0
  37. package/dist/chunk-HFQHU5LC.js +445 -0
  38. package/dist/chunk-HFQHU5LC.js.map +1 -0
  39. package/dist/chunk-IVHUBLJD.js +318 -0
  40. package/dist/chunk-IVHUBLJD.js.map +1 -0
  41. package/dist/chunk-KJWKY4L4.js +14 -0
  42. package/dist/chunk-KJWKY4L4.js.map +1 -0
  43. package/dist/chunk-MJVY2XUN.js +1793 -0
  44. package/dist/chunk-MJVY2XUN.js.map +1 -0
  45. package/dist/chunk-QWZVCJII.js +1797 -0
  46. package/dist/chunk-QWZVCJII.js.map +1 -0
  47. package/dist/chunk-VTSNRV3V.js +3237 -0
  48. package/dist/chunk-VTSNRV3V.js.map +1 -0
  49. package/dist/chunk-W5B4GTXR.js +1466 -0
  50. package/dist/chunk-W5B4GTXR.js.map +1 -0
  51. package/dist/chunk-WZEZLVOW.js +4995 -0
  52. package/dist/chunk-WZEZLVOW.js.map +1 -0
  53. package/dist/chunk-YPTTIXKC.js +199 -0
  54. package/dist/chunk-YPTTIXKC.js.map +1 -0
  55. package/dist/chunk-Z55UGM6X.js +6360 -0
  56. package/dist/chunk-Z55UGM6X.js.map +1 -0
  57. package/dist/chunk-ZIIRQODJ.js +110 -0
  58. package/dist/chunk-ZIIRQODJ.js.map +1 -0
  59. package/dist/chunk-ZSUQ4FMB.js +77 -0
  60. package/dist/chunk-ZSUQ4FMB.js.map +1 -0
  61. package/dist/client-JMTSZS3V.js +10 -0
  62. package/dist/client-JMTSZS3V.js.map +1 -0
  63. package/dist/deprecated-snap.js +19 -0
  64. package/dist/deprecated-snap.js.map +1 -0
  65. package/dist/dist-2KWBZFLA.js +14 -0
  66. package/dist/dist-2KWBZFLA.js.map +1 -0
  67. package/dist/dist-5ZYKNNU3.js +7 -0
  68. package/dist/dist-5ZYKNNU3.js.map +1 -0
  69. package/dist/dist-CP3RFHPI.js +11 -0
  70. package/dist/dist-CP3RFHPI.js.map +1 -0
  71. package/dist/gecko-53ITAGG6.js +56 -0
  72. package/dist/gecko-53ITAGG6.js.map +1 -0
  73. package/dist/guards-QAFC64NO.js +7 -0
  74. package/dist/guards-QAFC64NO.js.map +1 -0
  75. package/dist/index.js +57785 -0
  76. package/dist/index.js.map +1 -0
  77. package/dist/init-command-246JIVXM.js +7 -0
  78. package/dist/init-command-246JIVXM.js.map +1 -0
  79. package/dist/init-core-KAI7LCXZ.js +12 -0
  80. package/dist/init-core-KAI7LCXZ.js.map +1 -0
  81. package/dist/init-scan-RZNYDTUV.js +1919 -0
  82. package/dist/init-scan-RZNYDTUV.js.map +1 -0
  83. package/dist/local-service-adapter-6KNN6WQL.js +8 -0
  84. package/dist/local-service-adapter-6KNN6WQL.js.map +1 -0
  85. package/dist/secure-credentials-JXWAQLS2.js +306 -0
  86. package/dist/secure-credentials-JXWAQLS2.js.map +1 -0
  87. package/dist/tui-TPJPUS2R.js +111 -0
  88. package/dist/tui-TPJPUS2R.js.map +1 -0
  89. package/dist/vreko-dir-O3RLG7PI.js +8 -0
  90. package/dist/vreko-dir-O3RLG7PI.js.map +1 -0
  91. package/package.json +132 -0
  92. package/scripts/check-banned-words.ts +152 -0
  93. package/scripts/hooks/posttooluse-file-notify.sh +108 -0
  94. package/scripts/hooks/pretooluse-fragile-guard.sh +82 -0
  95. package/scripts/post-install-notice.js +24 -0
  96. package/scripts/postinstall.mjs +84 -0
  97. package/scripts/preuninstall.mjs +34 -0
  98. package/scripts/verify-jsx-transform.mjs +55 -0
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Banned Words Check
3
+ *
4
+ * Scans CLI source files for banned words in user-facing strings.
5
+ * Prevents brand terminology drift (snapback, daemon) in terminal output.
6
+ *
7
+ * Usage: node scripts/check-banned-words.ts
8
+ *
9
+ * @module scripts/check-banned-words
10
+ */
11
+
12
+ import { readdirSync, readFileSync } from "node:fs";
13
+ import { dirname, join } from "node:path";
14
+ import { fileURLToPath } from "node:url";
15
+
16
+ const __dirname = dirname(fileURLToPath(import.meta.url));
17
+ const cliSrcDir = join(__dirname, "../src");
18
+
19
+ // Banned words with their replacements
20
+ const BANNED_WORDS = [
21
+ { word: "snapback", replacement: "vreko", description: "Brand name" },
22
+ { word: "daemon", replacement: "service", description: "Service terminology" },
23
+ ];
24
+
25
+ // Patterns to exclude (internal code, not user-facing)
26
+ const EXCLUDE_PATTERNS = [
27
+ // File paths (directory names, log files)
28
+ /\.vreko\/daemon/,
29
+ /daemon\.log/,
30
+ /join\(.*daemon/,
31
+ /homedir\(\).*daemon/,
32
+ // Variable names
33
+ /daemonResult/,
34
+ /getDaemonPid/,
35
+ /isDaemon/,
36
+ /connectToDaemon/,
37
+ /getDaemonClient/,
38
+ /registerWithDaemon/,
39
+ /tryAutoStartDaemon/,
40
+ // Object properties
41
+ /result\.daemon/,
42
+ /client\.daemon/,
43
+ /daemonIcon/,
44
+ /daemonMajor/,
45
+ /daemonError/,
46
+ // Command names (the actual CLI command - users type this)
47
+ /program\.command\("daemon"\)/,
48
+ /command\("daemon"\)/,
49
+ /"daemon",/,
50
+ /process\.argv.*daemon/,
51
+ /vreko daemon start/,
52
+ // API methods (internal protocol)
53
+ /"daemon\/status"/,
54
+ /"daemon\/ping"/,
55
+ /"daemon\/start"/,
56
+ /"daemon\/shutdown"/,
57
+ // Help table command names (the command itself, not description)
58
+ /\{ name: "daemon"/,
59
+ // Test files
60
+ /__tests__/,
61
+ // Comments
62
+ /\/\/.*daemon/,
63
+ /\/\/.*snapback/,
64
+ ];
65
+
66
+ interface Violation {
67
+ file: string;
68
+ line: number;
69
+ word: string;
70
+ match: string;
71
+ replacement: string;
72
+ }
73
+
74
+ const violations: Violation[] = [];
75
+
76
+ function scanFile(filePath: string): void {
77
+ const content = readFileSync(filePath, "utf-8");
78
+ const lines = content.split("\n");
79
+
80
+ for (let i = 0; i < lines.length; i++) {
81
+ const line = lines[i];
82
+ const lineNum = i + 1;
83
+
84
+ // Skip if line contains any exclude pattern
85
+ if (EXCLUDE_PATTERNS.some((pattern) => pattern.test(line))) {
86
+ continue;
87
+ }
88
+
89
+ // Check for banned words in string literals
90
+ for (const { word, replacement } of BANNED_WORDS) {
91
+ // Match word boundaries in string literals
92
+ const stringPattern = new RegExp(`["\`]\\b${word}\\b["\`]`, "gi");
93
+ const matches = line.match(stringPattern);
94
+
95
+ if (matches) {
96
+ for (const match of matches) {
97
+ violations.push({
98
+ file: filePath.replace(cliSrcDir, "src"),
99
+ line: lineNum,
100
+ word,
101
+ match,
102
+ replacement,
103
+ });
104
+ }
105
+ }
106
+ }
107
+ }
108
+ }
109
+
110
+ function scanDirectory(dir: string): void {
111
+ const entries = readdirSync(dir, { withFileTypes: true });
112
+
113
+ for (const entry of entries) {
114
+ const fullPath = join(dir, entry.name);
115
+
116
+ if (entry.isDirectory()) {
117
+ // Recursively scan subdirectories
118
+ scanDirectory(fullPath);
119
+ } else if (entry.isFile() && entry.name.endsWith(".ts")) {
120
+ // Scan TypeScript files
121
+ scanFile(fullPath);
122
+ }
123
+ }
124
+ }
125
+
126
+ // Scan relevant directories
127
+ const targetDirs = ["commands", "ui", "services"];
128
+
129
+ for (const targetDir of targetDirs) {
130
+ const dirPath = join(cliSrcDir, targetDir);
131
+ try {
132
+ scanDirectory(dirPath);
133
+ } catch (error) {
134
+ // Directory might not exist, skip
135
+ void error;
136
+ }
137
+ }
138
+
139
+ // Report results
140
+ if (violations.length === 0) {
141
+ console.log("✅ No banned words found in user-facing strings");
142
+ process.exit(0);
143
+ } else {
144
+ console.error(`❌ Found ${violations.length} banned word violation(s):\n`);
145
+
146
+ for (const violation of violations) {
147
+ console.error(` ${violation.file}:${violation.line} - "${violation.match}" → use "${violation.replacement}"`);
148
+ }
149
+
150
+ console.error("\nFix these violations before committing.");
151
+ process.exit(1);
152
+ }
@@ -0,0 +1,108 @@
1
+ #!/usr/bin/env bash
2
+ # PostToolUse hook - file-notify IPC bridge
3
+ # Fires after every Edit/Write/MultiEdit and notifies the vrekod daemon via
4
+ # intelligence/file-modified IPC so fileIndex in workspace.json stays
5
+ # current for Claude Code users.
6
+ #
7
+ # Hook contract:
8
+ # stdin - JSON: { tool: { name, input: { file_path?, edits?, paths? } } }
9
+ # stdout - reserved (not written by this hook)
10
+ # stderr - surfaced to the model; kept silent on happy path
11
+ # exit 0 - always (PostToolUse hooks must never block)
12
+ #
13
+ # Degraded-state breadcrumb (R-FIX-3):
14
+ # The *success* path stays fire-and-forget for latency. But a missing
15
+ # dependency (jq/socat/nc) or an unreachable socket would otherwise make every
16
+ # agent edit vanish before reaching the daemon - silently - which is the
17
+ # phantom-action failure mode this platform exists to detect. So degraded
18
+ # conditions leave a breadcrumb at ~/.vreko/daemon/ingress-degraded that
19
+ # `vr status` reads and surfaces. Asserting on silence asserts on nothing.
20
+
21
+ # ---- Degraded-state breadcrumb -------------------------------------------
22
+ # Writes (or refreshes) a single-line marker describing why ingress degraded.
23
+ # Best-effort: the breadcrumb itself must never block or fail the hook.
24
+ DEGRADED_MARKER="$HOME/.vreko/daemon/ingress-degraded"
25
+ write_degraded_breadcrumb() {
26
+ reason="$1"
27
+ marker_dir="$(dirname "$DEGRADED_MARKER")"
28
+ mkdir -p "$marker_dir" 2>/dev/null || return 0
29
+ ts="$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || echo unknown)"
30
+ printf 'ingress-degraded reason=%s at=%s\n' "$reason" "$ts" >"$DEGRADED_MARKER" 2>/dev/null || true
31
+ }
32
+
33
+ # ---- Gate: jq is required to parse the hook payload ----------------------
34
+ # Without jq the payload cannot be parsed and the edit cannot be forwarded.
35
+ # This is a degraded condition, not a no-op.
36
+ if ! command -v jq >/dev/null 2>&1; then
37
+ write_degraded_breadcrumb "missing-dependency:jq"
38
+ exit 0
39
+ fi
40
+
41
+ # ---- Parse stdin ---------------------------------------------------------
42
+ INPUT=$(cat)
43
+ TOOL_NAME=$(printf '%s' "$INPUT" | jq -r '.tool.name // empty' 2>/dev/null || echo "")
44
+
45
+ # ---- Gate: only act on file-modifying tools ------------------------------
46
+ case "$TOOL_NAME" in
47
+ Edit|Write|MultiEdit) ;;
48
+ *) exit 0 ;;
49
+ esac
50
+
51
+ # ---- Extract target file -------------------------------------------------
52
+ # MultiEdit: .tool.input.edits[0].file_path takes priority
53
+ # Edit/Write: .tool.input.file_path
54
+ # Fallback: .tool.input.paths[0]
55
+ TARGET_FILE=$(printf '%s' "$INPUT" | jq -r '
56
+ .tool.input.edits[0].file_path //
57
+ .tool.input.file_path //
58
+ .tool.input.paths[0] //
59
+ empty
60
+ ' 2>/dev/null || echo "")
61
+
62
+ if [ -z "$TARGET_FILE" ]; then
63
+ exit 0
64
+ fi
65
+
66
+ # ---- Resolve workspace ---------------------------------------------------
67
+ # CLAUDE_PROJECT_DIR is set by Claude Code to the current project root.
68
+ WORKSPACE="${CLAUDE_PROJECT_DIR:-$(pwd)}"
69
+
70
+ # ---- Build JSON-RPC payload ----------------------------------------------
71
+ # NOTE: `aiTool` is the AI *client* identity, which the daemon's
72
+ # intelligence/file-modified schema constrains to a fixed enum
73
+ # (cursor|copilot|claude|windsurf|burst-pattern). This hook runs under Claude
74
+ # Code, so the identity is "claude". The Claude Code *tool* name ($TOOL_NAME,
75
+ # e.g. Edit/Write/MultiEdit) is NOT a valid `aiTool` value - sending it makes the
76
+ # daemon reject the payload with -32602 and, because this hook is fire-and-forget,
77
+ # the rejection would be silent. `aiAttributed:true` already marks the change as
78
+ # AI-driven; `aiTool:"claude"` records which assistant.
79
+ SOCK="$HOME/.vreko/service.sock"
80
+ PAYLOAD=$(printf '{"jsonrpc":"2.0","method":"intelligence/file-modified","params":{"workspace":"%s","path":"%s","aiAttributed":true,"aiTool":"claude"},"id":1}\n' \
81
+ "$WORKSPACE" "$TARGET_FILE")
82
+
83
+ # ---- Gate: a transport (socat or nc) is required -------------------------
84
+ # Without either, the edit cannot reach the daemon - degraded, not silent.
85
+ if ! command -v socat >/dev/null 2>&1 && ! command -v nc >/dev/null 2>&1; then
86
+ write_degraded_breadcrumb "missing-dependency:socat-or-nc"
87
+ exit 0
88
+ fi
89
+
90
+ # ---- Gate: the daemon socket must exist ----------------------------------
91
+ # A missing socket means the daemon is unreachable. Detecting it here (cheap,
92
+ # non-blocking) lets us leave a breadcrumb instead of firing into the void.
93
+ if [ ! -S "$SOCK" ]; then
94
+ write_degraded_breadcrumb "socket-unreachable"
95
+ exit 0
96
+ fi
97
+
98
+ # ---- Fire-and-forget IPC (2s timeout, latency-preserving) ----------------
99
+ # Deps present and socket exists: stay fire-and-forget on the success path.
100
+ if command -v socat >/dev/null 2>&1; then
101
+ { printf '%s' "$PAYLOAD" | socat -t2 UNIX-CONNECT:"$SOCK" -; } &>/dev/null &
102
+ disown 2>/dev/null || true
103
+ elif command -v nc >/dev/null 2>&1; then
104
+ { timeout 2 nc -U "$SOCK" <<< "$PAYLOAD" || true; } &>/dev/null &
105
+ disown 2>/dev/null || true
106
+ fi
107
+
108
+ exit 0
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env bash
2
+ # PreToolUse hook - fragile-file guard
3
+ # Reads stdin JSON, calls `vreko pulse`, emits stderr banner if target is fragile.
4
+ # Exit 0 = allow (advisory); Exit 2 = block (blocking mode only).
5
+ # Hook contract: stdout reserved for structured responses; stderr surfaces to model.
6
+
7
+ set -euo pipefail
8
+
9
+ # --- Parse stdin (Claude Code hook contract) -----------------------------
10
+ INPUT=$(cat)
11
+ TOOL_NAME=$(printf '%s' "$INPUT" | jq -r '.tool.name // empty' 2>/dev/null || echo "")
12
+ TARGET_FILE=$(printf '%s' "$INPUT" | jq -r '.tool.input.file_path // empty' 2>/dev/null || echo "")
13
+
14
+ # Only act on Edit/Write/MultiEdit operations
15
+ case "$TOOL_NAME" in
16
+ Edit|Write|MultiEdit) ;;
17
+ *) exit 0 ;;
18
+ esac
19
+
20
+ # Require a target file
21
+ if [ -z "$TARGET_FILE" ]; then
22
+ exit 0
23
+ fi
24
+
25
+ # --- Daemon presence check (graceful degradation) ------------------------
26
+ if ! vreko daemon status --quiet 2>/dev/null; then
27
+ exit 0
28
+ fi
29
+
30
+ # --- Pulse query (HOOK-T-03: bounded timeout) ----------------------------
31
+ if command -v timeout >/dev/null 2>&1; then
32
+ PULSE=$(timeout 5 vreko pulse --format json --focus "$TARGET_FILE" 2>/dev/null || echo '{}')
33
+ else
34
+ PULSE=$(vreko pulse --format json --focus "$TARGET_FILE" 2>/dev/null || echo '{}')
35
+ fi
36
+
37
+ # --- Determine fragility verdict ----------------------------------------
38
+ # Since --focus already filters to the target file, a non-empty fragileFiles
39
+ # array means the target IS fragile. Avoids path-matching ambiguity.
40
+ IS_FRAGILE=$(printf '%s' "$PULSE" | jq -r '(.fragileFiles | length) > 0' \
41
+ 2>/dev/null || echo "false")
42
+
43
+ if [ "$IS_FRAGILE" != "true" ]; then
44
+ exit 0
45
+ fi
46
+
47
+ # --- Compute provenance fields (HOOK-04) --------------------------------
48
+ if command -v shasum >/dev/null 2>&1; then
49
+ SHA=$(shasum -a 256 "$TARGET_FILE" 2>/dev/null | cut -c1-16 || echo "unknown")
50
+ elif command -v openssl >/dev/null 2>&1; then
51
+ SHA=$(openssl sha256 -r "$TARGET_FILE" 2>/dev/null | cut -c1-16 || echo "unknown")
52
+ else
53
+ SHA="unknown"
54
+ fi
55
+
56
+ CONFIDENCE=$(printf '%s' "$PULSE" | jq -r '.fragileFiles[0]?.fragility // 0' \
57
+ 2>/dev/null || echo "0")
58
+
59
+ HINT=$(printf '%s' "$PULSE" | jq -r '.llmHint // empty' 2>/dev/null || echo "")
60
+
61
+ # --- Mode resolution (HOOK-T-04: case literal compare, no eval) ---------
62
+ MODE=$(vreko config get hooks.claude-code.mode 2>/dev/null || echo "advisory")
63
+ case "$MODE" in
64
+ blocking) MODE="blocking" ;;
65
+ advisory|"") MODE="advisory" ;;
66
+ *) MODE="advisory" ;;
67
+ esac
68
+
69
+ # --- Emit user-visible output to stderr ---------------------------------
70
+ {
71
+ printf '⚠ Vreko: %s is flagged fragile.\n' "$TARGET_FILE"
72
+ printf ' SHA: %s Confidence: %s\n' "$SHA" "$CONFIDENCE"
73
+ if [ -n "$HINT" ]; then
74
+ printf '%s\n' "$HINT"
75
+ fi
76
+ } >&2
77
+
78
+ # --- Exit code ----------------------------------------------------------
79
+ if [ "$MODE" = "blocking" ]; then
80
+ exit 2
81
+ fi
82
+ exit 0
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Post-install notice for CLI binary name change
5
+ *
6
+ * Informs users about the snap → vr migration
7
+ */
8
+
9
+ const chalk = require("chalk");
10
+
11
+ console.log(chalk.cyan.bold("\n📦 Vreko CLI Installed!\n"));
12
+ console.log(chalk.yellow("⚠️ IMPORTANT: Command name change"));
13
+ console.log(chalk.gray(" Old: snap <command>"));
14
+ console.log(chalk.green(" New: vr <command> (recommended)"));
15
+ console.log(chalk.gray(" Or: vreko <command>"));
16
+ console.log();
17
+ console.log(chalk.dim('The "snap" command still works but will be removed in v4.0.0'));
18
+ console.log(chalk.dim("Update your scripts and aliases now to avoid disruption."));
19
+ console.log();
20
+ console.log(chalk.cyan("Quick start:"));
21
+ console.log(chalk.gray(" vr init # Initialize workspace"));
22
+ console.log(chalk.gray(" vr --help # View all commands"));
23
+ console.log();
24
+ console.log(chalk.dim("Migration guide: https://docs.vreko.dev/cli/migration\n"));
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Post-install script for @vreko/cli
5
+ * Runs after npm install to:
6
+ * 1. Emit the cli_installed analytics event (idempotent, once per machine).
7
+ * 2. Install the OS supervisor so the daemon auto-restarts on crash.
8
+ *
9
+ * CLI-EVENTS-02: Emits the `cli_installed` pioneer event idempotently.
10
+ * Uses a ~/.vreko/installed flag file to ensure the event fires only once
11
+ * per machine, regardless of how many times the package is installed.
12
+ *
13
+ * RULES: This script must ALWAYS exit 0. A postinstall failure breaks
14
+ * `npm install -g` entirely. Every code path must end with success.
15
+ * No telemetry is added beyond the existing cli_installed event.
16
+ */
17
+
18
+ import { spawnSync } from "node:child_process";
19
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
20
+ import { homedir, platform } from "node:os";
21
+ import { join } from "node:path";
22
+
23
+ // Idempotent cli_installed emit - fire once, then never again on this machine.
24
+ try {
25
+ const globalDir = join(homedir(), ".vreko");
26
+ const flagFile = join(globalDir, "installed");
27
+
28
+ if (!existsSync(flagFile)) {
29
+ // Ensure ~/.vreko/ exists
30
+ mkdirSync(globalDir, { recursive: true });
31
+ // Write timestamp flag before emitting so a crash/timeout doesn't re-emit
32
+ writeFileSync(flagFile, new Date().toISOString(), "utf8");
33
+
34
+ // Fire-and-forget POST - never await, never throw
35
+ const endpoint = process.env.VREKO_API_URL ?? "https://api.vreko.dev";
36
+ fetch(`${endpoint}/v1/analytics/events`, {
37
+ method: "POST",
38
+ headers: { "Content-Type": "application/json" },
39
+ body: JSON.stringify({ event: "cli_installed" }),
40
+ }).catch(() => {
41
+ // Silently discard - postinstall must never fail due to analytics
42
+ });
43
+ }
44
+ } catch {
45
+ // Non-fatal: postinstall must always succeed regardless of analytics errors
46
+ }
47
+
48
+ // Supervisor install - runs after the analytics section.
49
+ // Always exits 0. Never throws. Never fails the install.
50
+ try {
51
+ const currentPlatform = platform();
52
+
53
+ if (currentPlatform === "win32") {
54
+ // Windows is preview-only; supervisor install is manual.
55
+ process.stdout.write("vreko: Windows is preview; run `vreko service install` manually\n");
56
+ } else if (currentPlatform === "darwin" || currentPlatform === "linux") {
57
+ // Refuse to run supervisor install as root - launchd agents must be
58
+ // installed as the user who will own them, not as root.
59
+ const isRoot = typeof process.getuid === "function" && process.getuid() === 0;
60
+ if (isRoot) {
61
+ const user = process.env.SUDO_USER || process.env.USER || "your-user";
62
+ process.stdout.write(
63
+ `vreko: installed as root. Skipping supervisor install.\n` +
64
+ ` Run: sudo -u ${user} vreko service install\n`,
65
+ );
66
+ } else {
67
+ // npm creates bin symlinks before running postinstall, so `vreko` is on PATH.
68
+ // Use "pipe" so subprocess output doesn't interleave with our one-line status.
69
+ const result = spawnSync("vreko", ["service", "install"], {
70
+ stdio: "pipe",
71
+ shell: false,
72
+ });
73
+ if (result.status !== 0 || result.error) {
74
+ // Non-fatal: log one line, do not propagate error.
75
+ process.stderr.write("vreko: supervisor install failed; run `vreko service install` manually\n");
76
+ } else {
77
+ process.stdout.write("vreko: supervisor installed\n");
78
+ }
79
+ }
80
+ }
81
+ } catch {
82
+ // Any unexpected error is non-fatal - postinstall must always succeed.
83
+ process.stderr.write("vreko: supervisor install failed; run `vreko service install` manually\n");
84
+ }
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Pre-uninstall script for @vreko/cli
5
+ * Runs before npm uninstall to remove the OS supervisor agent/unit.
6
+ *
7
+ * RULES: This script must ALWAYS exit 0. A preuninstall failure blocks
8
+ * `npm uninstall -g`. Every code path must end with success.
9
+ */
10
+
11
+ import { spawnSync } from "node:child_process";
12
+ import { platform } from "node:os";
13
+
14
+ try {
15
+ const currentPlatform = platform();
16
+
17
+ if (currentPlatform === "win32") {
18
+ // Windows preview: no supervisor to uninstall.
19
+ } else if (currentPlatform === "darwin" || currentPlatform === "linux") {
20
+ const isRoot = typeof process.getuid === "function" && process.getuid() === 0;
21
+ if (!isRoot) {
22
+ const result = spawnSync("vreko", ["service", "uninstall"], {
23
+ stdio: "inherit",
24
+ shell: false,
25
+ });
26
+ if (result.status !== 0 || result.error) {
27
+ process.stderr.write("vreko: supervisor uninstall failed; run `vreko service uninstall` manually\n");
28
+ }
29
+ }
30
+ }
31
+ } catch {
32
+ // Any unexpected error is non-fatal.
33
+ process.stderr.write("vreko: supervisor uninstall failed; run `vreko service uninstall` manually\n");
34
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Post-build guard: asserts the CLI dist is using the automatic JSX transform.
3
+ *
4
+ * WHY THIS EXISTS:
5
+ * When tsconfig.base.json has "emitDecoratorMetadata": true, tsup switches from
6
+ * esbuild to SWC for transpilation. SWC's default JSX transform is classic
7
+ * (React.createElement) WITHOUT injecting "import React from 'react'". Combined
8
+ * with splitting:true, this produces ESM chunks where React is referenced but
9
+ * never imported → ReferenceError: React is not defined at runtime.
10
+ *
11
+ * The fix lives in tsup.config.ts: swc.jsc.transform.react.runtime = "automatic"
12
+ * This script verifies the fix is in effect after every build.
13
+ */
14
+
15
+ import { existsSync, readFileSync } from "node:fs";
16
+ import { dirname, join } from "node:path";
17
+ import { fileURLToPath } from "node:url";
18
+
19
+ const __dirname = dirname(fileURLToPath(import.meta.url));
20
+ const distIndex = join(__dirname, "../dist/index.js");
21
+ const distDir = join(__dirname, "../dist");
22
+
23
+ if (!existsSync(distIndex)) {
24
+ process.exit(1);
25
+ }
26
+
27
+ const { readdirSync } = await import("node:fs");
28
+ const jsFiles = readdirSync(distDir).filter((f) => f.endsWith(".js"));
29
+ const fileContents = jsFiles.map((file) => ({
30
+ file,
31
+ content: readFileSync(join(distDir, file), "utf-8"),
32
+ }));
33
+
34
+ // In split bundles, the automatic runtime import can live in a chunk rather than index.js.
35
+ const hasAutomaticJsxRuntime = fileContents.some(({ content }) => content.includes("react/jsx-runtime"));
36
+ if (!hasAutomaticJsxRuntime) {
37
+ process.exit(1);
38
+ }
39
+
40
+ // Guard against classic JSX output without React in scope.
41
+ for (const { content } of fileContents) {
42
+ const hasClassicJsx = content.includes("React.createElement");
43
+ const hasReactImport =
44
+ content.includes("import React") ||
45
+ content.includes('from "react"') ||
46
+ content.includes("from 'react'") ||
47
+ content.includes('import("react")') ||
48
+ content.includes("import('react')") ||
49
+ content.includes('require("react")') ||
50
+ content.includes("require('react')");
51
+
52
+ if (hasClassicJsx && !hasReactImport) {
53
+ process.exit(1);
54
+ }
55
+ }