syntropic 0.9.6 → 0.9.8
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/README.md +65 -15
- package/bin/syntropic.js +4 -0
- package/commands/analyse.js +22 -4
- package/commands/autofire.js +125 -0
- package/commands/init.js +165 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -27,8 +27,8 @@ You get: a disciplined development pipeline, governance doc templates, and a con
|
|
|
27
27
|
| Rule | What it prevents | Real-world save |
|
|
28
28
|
|------|-----------------|-----------------|
|
|
29
29
|
| **EG1: Pre-flight** | Localhost refs, broken builds reaching production | Caught 3 localhost references pre-deploy |
|
|
30
|
-
| **EG7: Pipeline** | AI jumping to code without research or planning | Structured
|
|
31
|
-
| **EG8: Test first** | Untested changes reaching production users | Every
|
|
30
|
+
| **EG7: Pipeline** | AI jumping to code without research or planning | Structured 210+ features across Full/Lightweight/Minimum cycles |
|
|
31
|
+
| **EG8: Test first** | Untested changes reaching production users | Every change verified on test page before promotion |
|
|
32
32
|
| **EG10: Env hygiene** | Env var corruption (trailing newlines, wrong formats) | Prevented silent API failures across 6 providers |
|
|
33
33
|
| **EG11: Prod sync** | Access control divergence between test and production | Caught lockout bug before 8+ users were affected |
|
|
34
34
|
| **EG14: Doc discipline** | Lost decisions, repeated mistakes, no project memory | Backlog, issues, and ADRs updated every cycle |
|
|
@@ -46,37 +46,74 @@ You get: a disciplined development pipeline, governance doc templates, and a con
|
|
|
46
46
|
|
|
47
47
|
**Health Check** — daily GitHub Action with auto-remediation (npm audit fix PRs).
|
|
48
48
|
|
|
49
|
+
## Deep Analysis
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
syntropic analyse
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Run a full venture analysis from your terminal. The CLI reads your codebase (stack, routes, data model, dependencies), sends a product profile to the SyntropicWorks server, and runs the analysis through **8 philosophy lenses**:
|
|
56
|
+
|
|
57
|
+
| Lens | What it evaluates |
|
|
58
|
+
|------|-------------------|
|
|
59
|
+
| Outcome Alignment | Is the business structurally coupled to customer success? |
|
|
60
|
+
| Simplicity | Can anything be removed without loss? |
|
|
61
|
+
| Growth & Bootstrapping | What's the zero-budget path to 1,000 users? |
|
|
62
|
+
| Assumption Testing | What's the cheapest way to disprove each key assumption? |
|
|
63
|
+
| Pricing Architecture | Does the tier structure move buyers from "whether" to "which"? |
|
|
64
|
+
| Deep Integration | If this disappeared tomorrow, what would break? |
|
|
65
|
+
| Capability Access | Who's excluded today, and what barriers gatekeep them? |
|
|
66
|
+
| Legal & Regulatory | What creates liability, and can compliance become a moat? |
|
|
67
|
+
|
|
68
|
+
**How it works:**
|
|
69
|
+
|
|
70
|
+
1. CLI auto-detects your project (package.json, routes, schema, infrastructure)
|
|
71
|
+
2. You describe your idea or venture
|
|
72
|
+
3. Server runs the full analysis — screening, report generation, 8 lens deep dives, validation plan
|
|
73
|
+
4. CLI shows real-time progress as each lens completes
|
|
74
|
+
5. Report displays in your terminal and saves to `.syntropic/reports/`
|
|
75
|
+
|
|
76
|
+
**Everything runs server-side.** Your idea text goes up, the finished report comes back. Analysis prompts, agent logic, and orchestration never leave the server.
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
syntropic analyse --idea "glamping site in the Cotswolds" # Non-interactive
|
|
80
|
+
syntropic analyse --focus "pricing strategy" # With a specific focus
|
|
81
|
+
syntropic analyse --list # Browse past analyses
|
|
82
|
+
syntropic analyse --dry-run # Preview product profile only
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Requires a free account (`syntropic login`).
|
|
86
|
+
|
|
49
87
|
## Track Record
|
|
50
88
|
|
|
51
|
-
Built in production on a real product (
|
|
89
|
+
Built in production on a real product (31 AI agents, 8-lens analyser, CLI, 8 decision tools, 92 API endpoints) over 7 months:
|
|
52
90
|
|
|
53
|
-
- **
|
|
54
|
-
- **
|
|
55
|
-
- **
|
|
91
|
+
- **210+ features shipped**, each through a structured pipeline
|
|
92
|
+
- **98% first-try success rate** across 42 tracked PRISM cycles
|
|
93
|
+
- **~370k tokens saved** — ~14k per complex cycle, ~9k lightweight, ~4k minimum
|
|
56
94
|
- **0 undetected production incidents** after methodology adoption
|
|
57
95
|
|
|
58
96
|
## Trust & Privacy
|
|
59
97
|
|
|
60
98
|
**Everything is local.** Your `.claude/` directory — rules, agents, governance docs — lives in your repo. It's yours.
|
|
61
99
|
|
|
62
|
-
**Telemetry is metadata only.** PRISM learns from anonymous structural metadata about development cycles — never from your code, file contents, or project details.
|
|
100
|
+
**Telemetry is metadata only.** PRISM learns from anonymous structural metadata about development cycles — never from your code, file contents, or project details.
|
|
63
101
|
|
|
64
102
|
**What's collected:** cycle weight, phases run, success/failure, tool used, OS, framework detected, file count bucket (1-5 / 6-20 / 21+), governance doc presence.
|
|
65
103
|
**Never collected:** file contents, file names, code, diffs, commit messages, project names, identity, API keys.
|
|
66
104
|
|
|
67
|
-
**Why it matters.**
|
|
105
|
+
**Why it matters.** Every anonymous report adds signal — which cycle weights work for which project shapes, where pipelines break down, what patterns lead to first-pass success. More contributors = better data = smarter rules for everyone.
|
|
68
106
|
|
|
69
|
-
**Contributors get more.** When telemetry is enabled, your PRISM sync fetches live network benchmarks — real success rates, iteration counts, and pattern intelligence computed from aggregate data across all contributors.
|
|
107
|
+
**Contributors get more.** When telemetry is enabled, your PRISM sync fetches live network benchmarks — real success rates, iteration counts, and pattern intelligence computed from aggregate data across all contributors. When disabled, you still get the full EG rules and agents, but benchmarks are static baselines only. No account needed — your identity is a double-hashed device fingerprint ([pseudonymisation details](https://zenodo.org/records/17894441)).
|
|
70
108
|
|
|
71
109
|
| | Community (telemetry off) | Contributor (telemetry on) |
|
|
72
110
|
|---|---|---|
|
|
73
111
|
| EG rules | Full | Full |
|
|
74
112
|
| Pipeline agents | Full | Full |
|
|
75
113
|
| Governance docs | Full | Full |
|
|
76
|
-
| Benchmarks | Static baselines | **Live network data**
|
|
77
|
-
| Pattern intelligence | Frozen snapshot | **Updated from aggregate reports**
|
|
78
|
-
| Cycle recommendations | Generic defaults | **Tuned by real data**
|
|
79
|
-
| Network benefit | None | **Your reports improve rules for everyone — including you** |
|
|
114
|
+
| Benchmarks | Static baselines | **Live network data** |
|
|
115
|
+
| Pattern intelligence | Frozen snapshot | **Updated from aggregate reports** |
|
|
116
|
+
| Cycle recommendations | Generic defaults | **Tuned by real data** |
|
|
80
117
|
|
|
81
118
|
Default: on. Opt out anytime:
|
|
82
119
|
|
|
@@ -96,15 +133,28 @@ syntropic remove
|
|
|
96
133
|
|
|
97
134
|
```bash
|
|
98
135
|
syntropic audit # Analyse git history (no install needed)
|
|
99
|
-
syntropic init [project-name] # Install methodology + docs
|
|
136
|
+
syntropic init [project-name] # Install methodology + docs + post-commit hook
|
|
100
137
|
syntropic add cursor windsurf # Add tools to existing project
|
|
101
138
|
syntropic remove # Clean uninstall (preserves docs)
|
|
102
139
|
syntropic health # Run pre-flight checks
|
|
103
140
|
syntropic report # Submit anonymous cycle report
|
|
141
|
+
syntropic autofire status # Check if post-commit hook is installed
|
|
142
|
+
syntropic autofire enable|disable # Manage the post-commit hook
|
|
104
143
|
syntropic telemetry status # Check telemetry setting
|
|
105
144
|
syntropic analyse # Deep-dive analysis (requires login)
|
|
106
|
-
syntropic analyse --list # Browse
|
|
145
|
+
syntropic analyse --list # Browse past analyses
|
|
146
|
+
syntropic analyse --dry-run # Preview product profile without sending
|
|
107
147
|
syntropic login # Sign in (needed for analyse only)
|
|
148
|
+
syntropic logout # Sign out
|
|
149
|
+
syntropic whoami # Show current auth status
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### `syntropic init` flags
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
syntropic init --no-hook # Skip installing the post-commit hook
|
|
156
|
+
syntropic init --no-email # Skip the optional email prompt
|
|
157
|
+
syntropic init --yes # Non-interactive (defaults to all tools, no email prompt)
|
|
108
158
|
```
|
|
109
159
|
|
|
110
160
|
## How It Works
|
package/bin/syntropic.js
CHANGED
|
@@ -61,6 +61,7 @@ const COMMANDS = {
|
|
|
61
61
|
health: () => require('../commands/health'),
|
|
62
62
|
telemetry: () => require('../commands/telemetry'),
|
|
63
63
|
report: () => require('../commands/report'),
|
|
64
|
+
autofire: () => require('../commands/autofire'),
|
|
64
65
|
login: () => require('../commands/login'),
|
|
65
66
|
logout: () => require('../commands/logout'),
|
|
66
67
|
whoami: () => require('../commands/whoami'),
|
|
@@ -92,6 +93,7 @@ if (!command || args.includes('--help') || args.includes('-h')) {
|
|
|
92
93
|
syntropic health Run a local health check
|
|
93
94
|
syntropic telemetry [cmd] Manage pseudonymised PRISM telemetry (enable/disable/status)
|
|
94
95
|
syntropic report [flags] Submit a PRISM cycle report
|
|
96
|
+
syntropic autofire [cmd] Manage post-commit hook that auto-fires report (enable/disable/status)
|
|
95
97
|
syntropic --version Show version
|
|
96
98
|
syntropic --help Show this help
|
|
97
99
|
|
|
@@ -107,6 +109,8 @@ if (!command || args.includes('--help') || args.includes('-h')) {
|
|
|
107
109
|
--domain example.com Production domain
|
|
108
110
|
--test-url /test Test page path
|
|
109
111
|
--prod-url / Production page path
|
|
112
|
+
--no-hook Skip installing post-commit autofire hook
|
|
113
|
+
--no-email Skip the optional email prompt
|
|
110
114
|
--yes Skip interactive prompts
|
|
111
115
|
|
|
112
116
|
Flags (analyse):
|
package/commands/analyse.js
CHANGED
|
@@ -504,13 +504,31 @@ async function runExecute(auth, profile, idea, focus, flags) {
|
|
|
504
504
|
const bar = '─'.repeat(50);
|
|
505
505
|
console.log(` ${bar} 100%\n`);
|
|
506
506
|
|
|
507
|
-
//
|
|
508
|
-
|
|
507
|
+
// Separate standard sections from partner outputs
|
|
508
|
+
const standardSections = (status.sections || []).filter(s => !s.slug?.startsWith('partner_output'));
|
|
509
|
+
const partnerSections = (status.sections || []).filter(s => s.slug?.startsWith('partner_output'));
|
|
509
510
|
|
|
510
|
-
//
|
|
511
|
-
|
|
511
|
+
// Display results (standard sections only)
|
|
512
|
+
displayReport(profile.project_name, standardSections);
|
|
513
|
+
|
|
514
|
+
// Save standard report
|
|
515
|
+
const savedPath = saveReport(profile.project_name, standardSections);
|
|
512
516
|
console.log(`\n Saved to: ${savedPath}`);
|
|
513
517
|
|
|
518
|
+
// Save partner outputs as separate files
|
|
519
|
+
for (const ps of partnerSections) {
|
|
520
|
+
try {
|
|
521
|
+
const jsonData = typeof ps.json_data === 'string' ? JSON.parse(ps.json_data) : (ps.json_data || {});
|
|
522
|
+
const filename = jsonData.output_filename || `${ps.type}.md`;
|
|
523
|
+
const downloadsDir = path.join(require('os').homedir(), 'Downloads');
|
|
524
|
+
const outputPath = path.join(downloadsDir, filename);
|
|
525
|
+
fs.writeFileSync(outputPath, ps.markdown, 'utf8');
|
|
526
|
+
console.log(` Partner output: ~/${path.relative(require('os').homedir(), outputPath)}`);
|
|
527
|
+
} catch (partnerErr) {
|
|
528
|
+
// Don't fail the whole flow for a partner output save error
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
514
532
|
// Feedback prompt
|
|
515
533
|
if (isInteractive) {
|
|
516
534
|
await promptFeedback(auth, sessionId);
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* syntropic autofire [enable|disable|status]
|
|
3
|
+
*
|
|
4
|
+
* Manages the git post-commit hook that fires `syntropic report` after each commit.
|
|
5
|
+
* Opt-in activation signal — turns "installed" into "used".
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
|
|
11
|
+
const MARKER = '# syntropic-autofire';
|
|
12
|
+
|
|
13
|
+
function hookPath(targetDir) {
|
|
14
|
+
return path.join(targetDir, '.git', 'hooks', 'post-commit');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function isInstalled(targetDir) {
|
|
18
|
+
const p = hookPath(targetDir);
|
|
19
|
+
if (!fs.existsSync(p)) return false;
|
|
20
|
+
try {
|
|
21
|
+
return fs.readFileSync(p, 'utf8').includes(MARKER);
|
|
22
|
+
} catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function install(targetDir) {
|
|
28
|
+
const gitDir = path.join(targetDir, '.git');
|
|
29
|
+
if (!fs.existsSync(gitDir)) {
|
|
30
|
+
return { ok: false, error: 'Not a git repository. Run `git init` first.' };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const hooksDir = path.join(gitDir, 'hooks');
|
|
34
|
+
if (!fs.existsSync(hooksDir)) fs.mkdirSync(hooksDir, { recursive: true });
|
|
35
|
+
|
|
36
|
+
const p = hookPath(targetDir);
|
|
37
|
+
const body = `#!/bin/sh
|
|
38
|
+
${MARKER}
|
|
39
|
+
# Auto-fires syntropic report after each commit. Non-blocking, fire-and-forget.
|
|
40
|
+
# Remove with: syntropic autofire disable
|
|
41
|
+
command -v syntropic >/dev/null 2>&1 || command -v npx >/dev/null 2>&1 || exit 0
|
|
42
|
+
(syntropic report >/dev/null 2>&1 || npx -y syntropic report >/dev/null 2>&1) &
|
|
43
|
+
exit 0
|
|
44
|
+
`;
|
|
45
|
+
|
|
46
|
+
if (fs.existsSync(p)) {
|
|
47
|
+
const existing = fs.readFileSync(p, 'utf8');
|
|
48
|
+
if (existing.includes(MARKER)) return { ok: true, already: true };
|
|
49
|
+
return { ok: false, error: 'A post-commit hook already exists. Remove it first or merge manually.' };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
fs.writeFileSync(p, body, { encoding: 'utf8', mode: 0o755 });
|
|
53
|
+
return { ok: true, already: false };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function uninstall(targetDir) {
|
|
57
|
+
const p = hookPath(targetDir);
|
|
58
|
+
if (!fs.existsSync(p)) return { ok: true, notFound: true };
|
|
59
|
+
try {
|
|
60
|
+
const content = fs.readFileSync(p, 'utf8');
|
|
61
|
+
if (!content.includes(MARKER)) {
|
|
62
|
+
return { ok: false, error: 'Post-commit hook exists but was not installed by syntropic. Not removed.' };
|
|
63
|
+
}
|
|
64
|
+
fs.unlinkSync(p);
|
|
65
|
+
return { ok: true, notFound: false };
|
|
66
|
+
} catch (err) {
|
|
67
|
+
return { ok: false, error: err.message };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function run(args) {
|
|
72
|
+
const sub = (args[0] || 'status').toLowerCase();
|
|
73
|
+
const targetDir = process.cwd();
|
|
74
|
+
|
|
75
|
+
if (sub === 'enable') {
|
|
76
|
+
const res = install(targetDir);
|
|
77
|
+
if (!res.ok) {
|
|
78
|
+
console.log(`\n ${res.error}\n`);
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
if (res.already) {
|
|
82
|
+
console.log('\n Autofire is already enabled.\n');
|
|
83
|
+
} else {
|
|
84
|
+
console.log('\n Autofire enabled. `syntropic report` will run after every commit (non-blocking).');
|
|
85
|
+
console.log(' Disable anytime: syntropic autofire disable\n');
|
|
86
|
+
}
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (sub === 'disable') {
|
|
91
|
+
const res = uninstall(targetDir);
|
|
92
|
+
if (!res.ok) {
|
|
93
|
+
console.log(`\n ${res.error}\n`);
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
if (res.notFound) {
|
|
97
|
+
console.log('\n Autofire was not installed. Nothing to remove.\n');
|
|
98
|
+
} else {
|
|
99
|
+
console.log('\n Autofire disabled. Post-commit hook removed.');
|
|
100
|
+
console.log(' Re-enable anytime: syntropic autofire enable\n');
|
|
101
|
+
}
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// status
|
|
106
|
+
const installed = isInstalled(targetDir);
|
|
107
|
+
console.log(`
|
|
108
|
+
Syntropic Autofire
|
|
109
|
+
──────────────────
|
|
110
|
+
Status: ${installed ? 'ENABLED' : 'DISABLED'}
|
|
111
|
+
Path: .git/hooks/post-commit
|
|
112
|
+
|
|
113
|
+
What it does:
|
|
114
|
+
Runs \`syntropic report\` after every git commit — a signal to the PRISM
|
|
115
|
+
network that a development cycle completed. Non-blocking, fire-and-forget.
|
|
116
|
+
Respects \`syntropic telemetry disable\`.
|
|
117
|
+
|
|
118
|
+
Commands:
|
|
119
|
+
syntropic autofire enable Install the post-commit hook in this repo
|
|
120
|
+
syntropic autofire disable Remove the hook
|
|
121
|
+
syntropic autofire status Show current state
|
|
122
|
+
`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
module.exports = run;
|
package/commands/init.js
CHANGED
|
@@ -14,7 +14,138 @@
|
|
|
14
14
|
const fs = require('fs');
|
|
15
15
|
const path = require('path');
|
|
16
16
|
const readline = require('readline');
|
|
17
|
-
const
|
|
17
|
+
const https = require('https');
|
|
18
|
+
const crypto = require('crypto');
|
|
19
|
+
const { ensureConfig, loadConfig, clientHash } = require('./config-utils');
|
|
20
|
+
|
|
21
|
+
const API_BASE = 'https://www.syntropicworks.com';
|
|
22
|
+
|
|
23
|
+
function postJson(urlPath, body, timeoutMs = 3000) {
|
|
24
|
+
return new Promise((resolve) => {
|
|
25
|
+
try {
|
|
26
|
+
const url = new URL(urlPath, API_BASE);
|
|
27
|
+
const data = JSON.stringify(body);
|
|
28
|
+
const req = https.request({
|
|
29
|
+
hostname: url.hostname,
|
|
30
|
+
port: 443,
|
|
31
|
+
path: url.pathname,
|
|
32
|
+
method: 'POST',
|
|
33
|
+
headers: {
|
|
34
|
+
'Content-Type': 'application/json',
|
|
35
|
+
'Content-Length': Buffer.byteLength(data),
|
|
36
|
+
},
|
|
37
|
+
timeout: timeoutMs,
|
|
38
|
+
}, (res) => {
|
|
39
|
+
res.resume();
|
|
40
|
+
res.on('end', () => resolve(res.statusCode >= 200 && res.statusCode < 300));
|
|
41
|
+
});
|
|
42
|
+
req.on('error', () => resolve(false));
|
|
43
|
+
req.on('timeout', () => { req.destroy(); resolve(false); });
|
|
44
|
+
req.write(data);
|
|
45
|
+
req.end();
|
|
46
|
+
} catch {
|
|
47
|
+
resolve(false);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function detectToolAndOs() {
|
|
53
|
+
let tool = 'unknown';
|
|
54
|
+
if (process.env.CURSOR_SESSION_ID || process.env.CURSOR_TRACE_ID) tool = 'cursor';
|
|
55
|
+
else if (process.env.WINDSURF_SESSION) tool = 'windsurf';
|
|
56
|
+
else if (process.env.GITHUB_COPILOT) tool = 'copilot';
|
|
57
|
+
else if (process.env.CODEX_SESSION) tool = 'codex';
|
|
58
|
+
else if (process.env.CLAUDECODE || process.env.CLAUDE_CODE_SESSION) tool = 'claude_code';
|
|
59
|
+
return { tool, os: require('os').platform() };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function detectFramework(dir) {
|
|
63
|
+
const has = (f) => fs.existsSync(path.join(dir, f));
|
|
64
|
+
if (has('next.config.js') || has('next.config.mjs') || has('next.config.ts')) return 'nextjs';
|
|
65
|
+
if (has('nuxt.config.js') || has('nuxt.config.ts')) return 'nuxt';
|
|
66
|
+
if (has('angular.json')) return 'angular';
|
|
67
|
+
if (has('svelte.config.js')) return 'svelte';
|
|
68
|
+
if (has('vite.config.js') || has('vite.config.ts')) return 'vite';
|
|
69
|
+
if (has('requirements.txt') || has('pyproject.toml')) return 'python';
|
|
70
|
+
if (has('Cargo.toml')) return 'rust';
|
|
71
|
+
if (has('go.mod')) return 'go';
|
|
72
|
+
return 'unknown';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function repoHashFor(dir) {
|
|
76
|
+
try {
|
|
77
|
+
const { execSync } = require('child_process');
|
|
78
|
+
let identity;
|
|
79
|
+
try {
|
|
80
|
+
identity = execSync('git remote get-url origin 2>/dev/null', { cwd: dir, encoding: 'utf8' }).trim();
|
|
81
|
+
} catch {
|
|
82
|
+
identity = path.basename(dir);
|
|
83
|
+
}
|
|
84
|
+
return crypto.createHash('sha256').update(identity || path.basename(dir)).digest('hex').slice(0, 16);
|
|
85
|
+
} catch {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function sendActivationPing(targetDir, selectedTools, hasHook) {
|
|
91
|
+
const config = loadConfig();
|
|
92
|
+
if (!config.telemetry) return;
|
|
93
|
+
const env = detectToolAndOs();
|
|
94
|
+
await postJson('/api/v1/prism/report', {
|
|
95
|
+
type: 'activation',
|
|
96
|
+
anon_id: clientHash(config.device_id),
|
|
97
|
+
cli_version: require('../package.json').version,
|
|
98
|
+
tool: env.tool,
|
|
99
|
+
os: env.os,
|
|
100
|
+
framework: detectFramework(targetDir),
|
|
101
|
+
repo_hash: repoHashFor(targetDir),
|
|
102
|
+
tools: selectedTools,
|
|
103
|
+
has_hook: !!hasHook,
|
|
104
|
+
source: 'init',
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function sendWaitlistEmail(email, targetDir) {
|
|
109
|
+
const config = loadConfig();
|
|
110
|
+
const env = detectToolAndOs();
|
|
111
|
+
await postJson('/api/v1/cli/waitlist', {
|
|
112
|
+
email,
|
|
113
|
+
anon_id: config.telemetry ? clientHash(config.device_id) : null,
|
|
114
|
+
cli_version: require('../package.json').version,
|
|
115
|
+
tool: env.tool,
|
|
116
|
+
os: env.os,
|
|
117
|
+
framework: detectFramework(targetDir),
|
|
118
|
+
source: 'init',
|
|
119
|
+
}, 5000);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function installPostCommitHook(targetDir) {
|
|
123
|
+
const gitDir = path.join(targetDir, '.git');
|
|
124
|
+
if (!fs.existsSync(gitDir)) return { installed: false, reason: 'not a git repo' };
|
|
125
|
+
|
|
126
|
+
const hooksDir = path.join(gitDir, 'hooks');
|
|
127
|
+
if (!fs.existsSync(hooksDir)) fs.mkdirSync(hooksDir, { recursive: true });
|
|
128
|
+
|
|
129
|
+
const hookPath = path.join(hooksDir, 'post-commit');
|
|
130
|
+
const marker = '# syntropic-autofire';
|
|
131
|
+
const body = `#!/bin/sh
|
|
132
|
+
${marker}
|
|
133
|
+
# Auto-fires syntropic report after each commit. Non-blocking, fire-and-forget.
|
|
134
|
+
# Remove with: syntropic autofire disable
|
|
135
|
+
command -v syntropic >/dev/null 2>&1 || command -v npx >/dev/null 2>&1 || exit 0
|
|
136
|
+
(syntropic report >/dev/null 2>&1 || npx -y syntropic report >/dev/null 2>&1) &
|
|
137
|
+
exit 0
|
|
138
|
+
`;
|
|
139
|
+
|
|
140
|
+
if (fs.existsSync(hookPath)) {
|
|
141
|
+
const existing = fs.readFileSync(hookPath, 'utf8');
|
|
142
|
+
if (existing.includes(marker)) return { installed: true, reason: 'already installed' };
|
|
143
|
+
return { installed: false, reason: 'post-commit hook exists (not overwritten)' };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
fs.writeFileSync(hookPath, body, { encoding: 'utf8', mode: 0o755 });
|
|
147
|
+
return { installed: true, reason: 'new hook' };
|
|
148
|
+
}
|
|
18
149
|
|
|
19
150
|
const TEMPLATES_DIR = path.join(__dirname, '..', 'templates');
|
|
20
151
|
|
|
@@ -316,6 +447,39 @@ async function run(args) {
|
|
|
316
447
|
}
|
|
317
448
|
}
|
|
318
449
|
|
|
450
|
+
// Install post-commit hook (auto-fires `syntropic report` after every commit)
|
|
451
|
+
// Skipped with --no-hook flag or when not a git repo
|
|
452
|
+
let hookResult = { installed: false, reason: 'skipped' };
|
|
453
|
+
if (!flags['no-hook']) {
|
|
454
|
+
hookResult = installPostCommitHook(targetDir);
|
|
455
|
+
if (hookResult.installed) {
|
|
456
|
+
console.log(' hook .git/hooks/post-commit (auto-fires `syntropic report` after each commit)');
|
|
457
|
+
} else if (hookResult.reason === 'not a git repo') {
|
|
458
|
+
console.log(' skip post-commit hook (not a git repo yet — run `git init` then `syntropic autofire enable`)');
|
|
459
|
+
} else if (hookResult.reason !== 'already installed') {
|
|
460
|
+
console.log(` skip post-commit hook (${hookResult.reason})`);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Optional email capture — helps measure real users vs npm install noise
|
|
465
|
+
// Only prompts when interactive and user hasn't passed --no-email
|
|
466
|
+
if (isInteractive && !flags['no-email']) {
|
|
467
|
+
console.log('');
|
|
468
|
+
const email = await ask(' Optional: email for roadmap updates + early access to paid features (Enter to skip): ');
|
|
469
|
+
if (email && email.includes('@') && email.length < 254) {
|
|
470
|
+
try {
|
|
471
|
+
await sendWaitlistEmail(email.trim(), targetDir);
|
|
472
|
+
console.log(' Thanks. No spam — just occasional updates.');
|
|
473
|
+
} catch { /* fire-and-forget */ }
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Fire activation ping — measures real `init` completion, not just npm pulls
|
|
478
|
+
// Respects telemetry opt-out
|
|
479
|
+
try {
|
|
480
|
+
await sendActivationPing(targetDir, selectedTools, hookResult.installed);
|
|
481
|
+
} catch { /* fire-and-forget, never block init */ }
|
|
482
|
+
|
|
319
483
|
console.log(`
|
|
320
484
|
Done! Your project is set up with the Syntropic pipeline.
|
|
321
485
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "syntropic",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.8",
|
|
4
4
|
"description": "Ship better software with a proven development methodology. Audit your git history, install disciplined rules, and track iterations — for Claude Code, Cursor, Windsurf, GitHub Copilot, and OpenAI Codex.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"syntropic": "./bin/syntropic.js"
|