@oaklandzoo/ostup 0.7.0 → 0.9.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/README.md +60 -39
- package/bin/cli.mjs +63 -1
- package/package.json +2 -1
- package/scripts/install.ps1 +115 -0
- package/scripts/install.sh +144 -0
- package/src/bootstrap.mjs +201 -0
- package/src/credential-prompts.mjs +80 -15
- package/src/doctor.mjs +188 -0
- package/src/exec.mjs +48 -11
- package/src/mvp-flow.mjs +6 -1
- package/src/preflight.mjs +3 -6
- package/src/tool-registry.mjs +164 -0
- package/src/tracer.mjs +116 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// tool-registry.mjs: single source of truth for the binaries ostup needs (node, git, gh, vercel).
|
|
2
|
+
// Detection logic lives here. Install commands per platform live here. Anything that wants to know
|
|
3
|
+
// "what does the user need installed" reads from REGISTRY. Avoids drift between preflight, bootstrap,
|
|
4
|
+
// and doctor.
|
|
5
|
+
import { execSync } from 'node:child_process';
|
|
6
|
+
|
|
7
|
+
export const REGISTRY = [
|
|
8
|
+
{
|
|
9
|
+
id: 'node',
|
|
10
|
+
name: 'Node',
|
|
11
|
+
blurb: 'The engine that runs the website setup tool (ostup).',
|
|
12
|
+
detect: { cmd: 'node', args: ['--version'], minMajor: 20 },
|
|
13
|
+
fix: 'Install Node 20+ from https://nodejs.org',
|
|
14
|
+
install: {
|
|
15
|
+
darwin: {
|
|
16
|
+
kind: 'manual',
|
|
17
|
+
url: 'https://nodejs.org',
|
|
18
|
+
note: 'The Mac installer (install.sh) handles Node via Homebrew. Re-run the bash installer to get it.',
|
|
19
|
+
},
|
|
20
|
+
linux: {
|
|
21
|
+
kind: 'manual',
|
|
22
|
+
url: 'https://nodejs.org',
|
|
23
|
+
note: 'Install Node 20+ from nodejs.org or your package manager.',
|
|
24
|
+
},
|
|
25
|
+
win32: {
|
|
26
|
+
kind: 'manual',
|
|
27
|
+
url: 'https://nodejs.org',
|
|
28
|
+
note: 'The Windows installer (install.ps1) handles Node via WinGet. Re-run the PowerShell installer to get it.',
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: 'git',
|
|
34
|
+
name: 'Git',
|
|
35
|
+
blurb: 'Saves versions of your project as you work.',
|
|
36
|
+
detect: { cmd: 'git', args: ['--version'] },
|
|
37
|
+
fix: 'Install git from https://git-scm.com',
|
|
38
|
+
install: {
|
|
39
|
+
darwin: {
|
|
40
|
+
kind: 'brew',
|
|
41
|
+
cmd: 'brew',
|
|
42
|
+
args: ['install', 'git'],
|
|
43
|
+
postPath: ['/opt/homebrew/bin', '/usr/local/bin'],
|
|
44
|
+
},
|
|
45
|
+
linux: {
|
|
46
|
+
kind: 'apt',
|
|
47
|
+
cmd: 'apt-get',
|
|
48
|
+
args: ['install', '-y', 'git'],
|
|
49
|
+
requiresRoot: true,
|
|
50
|
+
url: 'https://git-scm.com',
|
|
51
|
+
},
|
|
52
|
+
win32: {
|
|
53
|
+
kind: 'winget',
|
|
54
|
+
cmd: 'winget',
|
|
55
|
+
args: ['install', '-e', '--id', 'Git.Git', '--accept-package-agreements', '--accept-source-agreements'],
|
|
56
|
+
postPath: ['C:\\Program Files\\Git\\cmd'],
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
id: 'gh',
|
|
62
|
+
name: 'GitHub CLI',
|
|
63
|
+
blurb: 'Saves your project on GitHub so it is not just on your laptop.',
|
|
64
|
+
detect: { cmd: 'gh', args: ['--version'] },
|
|
65
|
+
fix: 'Install: brew install gh',
|
|
66
|
+
install: {
|
|
67
|
+
darwin: {
|
|
68
|
+
kind: 'brew',
|
|
69
|
+
cmd: 'brew',
|
|
70
|
+
args: ['install', 'gh'],
|
|
71
|
+
postPath: ['/opt/homebrew/bin', '/usr/local/bin'],
|
|
72
|
+
},
|
|
73
|
+
linux: {
|
|
74
|
+
kind: 'apt',
|
|
75
|
+
cmd: 'apt-get',
|
|
76
|
+
args: ['install', '-y', 'gh'],
|
|
77
|
+
requiresRoot: true,
|
|
78
|
+
url: 'https://cli.github.com',
|
|
79
|
+
},
|
|
80
|
+
win32: {
|
|
81
|
+
kind: 'winget',
|
|
82
|
+
cmd: 'winget',
|
|
83
|
+
args: ['install', '-e', '--id', 'GitHub.cli', '--accept-package-agreements', '--accept-source-agreements'],
|
|
84
|
+
postPath: ['C:\\Program Files\\GitHub CLI'],
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
id: 'vercel',
|
|
90
|
+
name: 'Vercel CLI',
|
|
91
|
+
blurb: 'Publishes your site to a public URL on the internet.',
|
|
92
|
+
detect: { cmd: 'vercel', args: ['--version'] },
|
|
93
|
+
fix: 'Install: npm i -g vercel',
|
|
94
|
+
install: {
|
|
95
|
+
darwin: {
|
|
96
|
+
kind: 'npm-g',
|
|
97
|
+
cmd: 'npm',
|
|
98
|
+
args: ['install', '-g', 'vercel'],
|
|
99
|
+
},
|
|
100
|
+
linux: {
|
|
101
|
+
kind: 'npm-g',
|
|
102
|
+
cmd: 'npm',
|
|
103
|
+
args: ['install', '-g', 'vercel'],
|
|
104
|
+
note: 'Stock apt-installed Node may need sudo for global installs.',
|
|
105
|
+
},
|
|
106
|
+
win32: {
|
|
107
|
+
kind: 'npm-g',
|
|
108
|
+
cmd: 'npm',
|
|
109
|
+
args: ['install', '-g', 'vercel'],
|
|
110
|
+
note: '%APPDATA%\\npm is on PATH by default with WinGet-installed Node.',
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
];
|
|
115
|
+
|
|
116
|
+
// Back-compat shape for preflight.mjs (and test/preflight.test.mjs which imports CHECKS).
|
|
117
|
+
// Keep the array shape: { name, cmd: [bin, ...args], fix, minNodeMajor? }.
|
|
118
|
+
export const CHECKS = REGISTRY.map((tool) => ({
|
|
119
|
+
name: tool.name,
|
|
120
|
+
cmd: [tool.detect.cmd, ...(tool.detect.args || [])],
|
|
121
|
+
fix: tool.fix,
|
|
122
|
+
...(tool.detect.minMajor ? { minNodeMajor: tool.detect.minMajor } : {}),
|
|
123
|
+
}));
|
|
124
|
+
|
|
125
|
+
function defaultRunner(cmd, args = []) {
|
|
126
|
+
return execSync([cmd, ...args].join(' '), { stdio: ['ignore', 'pipe', 'ignore'] }).toString();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function detectAll({ runner = defaultRunner } = {}) {
|
|
130
|
+
const found = [];
|
|
131
|
+
const missing = [];
|
|
132
|
+
for (const tool of REGISTRY) {
|
|
133
|
+
let out;
|
|
134
|
+
try {
|
|
135
|
+
out = runner(tool.detect.cmd, tool.detect.args || []);
|
|
136
|
+
} catch {
|
|
137
|
+
missing.push({ tool, reason: 'not-found' });
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (tool.detect.minMajor) {
|
|
141
|
+
const major = parseInt(String(out).replace(/^v/, '').split('.')[0], 10);
|
|
142
|
+
if (!Number.isFinite(major) || major < tool.detect.minMajor) {
|
|
143
|
+
missing.push({ tool, reason: 'version-too-old', detected: String(out).trim() });
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
found.push({ tool, version: String(out).trim() });
|
|
148
|
+
}
|
|
149
|
+
return { found, missing };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function installCommandFor(toolOrId, platform = process.platform) {
|
|
153
|
+
const tool = typeof toolOrId === 'string'
|
|
154
|
+
? REGISTRY.find((t) => t.id === toolOrId)
|
|
155
|
+
: toolOrId;
|
|
156
|
+
if (!tool) return null;
|
|
157
|
+
const plan = tool.install[platform];
|
|
158
|
+
if (plan) return plan;
|
|
159
|
+
return {
|
|
160
|
+
kind: 'manual',
|
|
161
|
+
url: tool.install.linux?.url || tool.install.darwin?.url || 'https://nodejs.org',
|
|
162
|
+
note: `No automatic install for platform ${platform}. Install ${tool.name} manually.`,
|
|
163
|
+
};
|
|
164
|
+
}
|
package/src/tracer.mjs
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// tracer.mjs: structured per-run log file. Writes every step + output to ~/.ostup/logs/<run-id>.log.
|
|
2
|
+
// Auto-enabled on every ostup init / brief / export-pro run unless --no-log is passed.
|
|
3
|
+
|
|
4
|
+
import { existsSync } from 'node:fs';
|
|
5
|
+
import { mkdir, appendFile } from 'node:fs/promises';
|
|
6
|
+
import { homedir } from 'node:os';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
|
|
9
|
+
let _enabled = true;
|
|
10
|
+
let _logPath = null;
|
|
11
|
+
let _runId = null;
|
|
12
|
+
|
|
13
|
+
export function disableTracer() {
|
|
14
|
+
_enabled = false;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function enableTracer() {
|
|
18
|
+
_enabled = true;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function isTracerEnabled() {
|
|
22
|
+
return _enabled;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function startRun({ label = 'run' } = {}) {
|
|
26
|
+
if (!_enabled) return null;
|
|
27
|
+
_runId = `${label}-${new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19)}`;
|
|
28
|
+
const dir = join(homedir(), '.ostup', 'logs');
|
|
29
|
+
await mkdir(dir, { recursive: true });
|
|
30
|
+
_logPath = join(dir, `${_runId}.log`);
|
|
31
|
+
await appendFile(_logPath, `# ostup run ${_runId}\n# started: ${new Date().toISOString()}\n# cwd: ${process.cwd()}\n# argv: ${process.argv.slice(2).join(' ')}\n\n`);
|
|
32
|
+
return _logPath;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function trace(category, message) {
|
|
36
|
+
if (!_enabled || !_logPath) return;
|
|
37
|
+
const stamp = new Date().toISOString();
|
|
38
|
+
const line = `[${stamp}] [${category}] ${message}\n`;
|
|
39
|
+
try {
|
|
40
|
+
await appendFile(_logPath, line);
|
|
41
|
+
} catch {
|
|
42
|
+
// Logging must never crash the main flow.
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function traceStep({ step, cmd, args = [], stdout = '', stderr = '', exitCode = 0, durationMs = 0 }) {
|
|
47
|
+
if (!_enabled || !_logPath) return;
|
|
48
|
+
const display = `${cmd} ${(args || []).join(' ')}`.trim();
|
|
49
|
+
const stdoutTail = (stdout || '').toString().split('\n').slice(-30).join('\n');
|
|
50
|
+
const stderrTail = (stderr || '').toString().split('\n').slice(-30).join('\n');
|
|
51
|
+
const lines = [
|
|
52
|
+
`## step ${step} | exit=${exitCode} | ${durationMs}ms`,
|
|
53
|
+
` cmd: ${display}`,
|
|
54
|
+
];
|
|
55
|
+
if (stdoutTail.trim()) {
|
|
56
|
+
lines.push(` stdout (last 30 lines):`);
|
|
57
|
+
for (const l of stdoutTail.split('\n')) lines.push(` ${l}`);
|
|
58
|
+
}
|
|
59
|
+
if (stderrTail.trim()) {
|
|
60
|
+
lines.push(` stderr (last 30 lines):`);
|
|
61
|
+
for (const l of stderrTail.split('\n')) lines.push(` ${l}`);
|
|
62
|
+
}
|
|
63
|
+
lines.push('');
|
|
64
|
+
try {
|
|
65
|
+
await appendFile(_logPath, lines.join('\n'));
|
|
66
|
+
} catch {
|
|
67
|
+
// ignore
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function traceError(category, err) {
|
|
72
|
+
if (!_enabled || !_logPath) return;
|
|
73
|
+
const stamp = new Date().toISOString();
|
|
74
|
+
const lines = [
|
|
75
|
+
`## ERROR [${stamp}] [${category}]`,
|
|
76
|
+
` message: ${err?.message || err}`,
|
|
77
|
+
` code: ${err?.code || '(none)'}`,
|
|
78
|
+
];
|
|
79
|
+
if (err?.cause) {
|
|
80
|
+
lines.push(` cause:`);
|
|
81
|
+
const causeStr = (err.cause.message || String(err.cause)).split('\n');
|
|
82
|
+
for (const l of causeStr.slice(0, 20)) lines.push(` ${l}`);
|
|
83
|
+
}
|
|
84
|
+
if (err?.stack) {
|
|
85
|
+
lines.push(` stack (first 10 lines):`);
|
|
86
|
+
for (const l of err.stack.split('\n').slice(0, 10)) lines.push(` ${l}`);
|
|
87
|
+
}
|
|
88
|
+
lines.push('');
|
|
89
|
+
try {
|
|
90
|
+
await appendFile(_logPath, lines.join('\n'));
|
|
91
|
+
} catch {
|
|
92
|
+
// ignore
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function endRun({ ok = true } = {}) {
|
|
97
|
+
if (!_enabled || !_logPath) return null;
|
|
98
|
+
const stamp = new Date().toISOString();
|
|
99
|
+
try {
|
|
100
|
+
await appendFile(_logPath, `\n# ended: ${stamp}\n# status: ${ok ? 'ok' : 'failed'}\n`);
|
|
101
|
+
} catch {
|
|
102
|
+
// ignore
|
|
103
|
+
}
|
|
104
|
+
const path = _logPath;
|
|
105
|
+
_logPath = null;
|
|
106
|
+
_runId = null;
|
|
107
|
+
return path;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function getLogPath() {
|
|
111
|
+
return _logPath;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function getRunId() {
|
|
115
|
+
return _runId;
|
|
116
|
+
}
|