lararium 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +36 -0
  2. package/index.js +179 -0
  3. package/package.json +26 -0
package/README.md ADDED
@@ -0,0 +1,36 @@
1
+ # lararium
2
+
3
+ The one-line door into the [lararium](https://github.com/kylnor/lararium) template. It
4
+ fetches the latest release, unpacks it into a new folder, and gets out of the way. No dependencies,
5
+ node stdlib only.
6
+
7
+ ```
8
+ npx lararium [folder]
9
+ ```
10
+
11
+ That's the whole tool. It resolves the newest tagged release from GitHub, downloads that tarball,
12
+ unpacks it into `folder` (default `lararium`), strips its own scaffolder code out of the copy,
13
+ runs `git init` with a first commit, and prints what to do next:
14
+
15
+ ```
16
+ cd <folder>
17
+ claude
18
+ ```
19
+
20
+ Then say: "Run the install interview in `INSTALL.md`". That interview is the actual install; this
21
+ package only gets the files onto your disk.
22
+
23
+ **Note on versioning:** this package's own version (`0.1.0` and up) is unrelated to the template's
24
+ `STACK_VERSION`. The scaffolder always fetches whatever the latest template release is at run time;
25
+ bumping this package is only for fixing the scaffolder itself.
26
+
27
+ ## Publishing (for the template's owner)
28
+
29
+ Requires an npm login with publish rights on the `lararium` package name. From this directory:
30
+
31
+ ```
32
+ npm publish
33
+ ```
34
+
35
+ Bump `version` in `package.json` first if anything in `index.js` changed. The published package
36
+ version stays decoupled from `STACK_VERSION`; do not try to keep them in sync.
package/index.js ADDED
@@ -0,0 +1,179 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ // lararium scaffolder. Node stdlib only, zero dependencies.
5
+ // Fetches the latest release of the lararium template and unpacks it
6
+ // into a new folder. See README.md in this package for the three-step story.
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+ const os = require('os');
11
+ const https = require('https');
12
+ const { execFileSync } = require('child_process');
13
+
14
+ const OWNER = 'kylnor';
15
+ const REPO = 'lararium';
16
+ const USER_AGENT = 'lararium-scaffolder';
17
+ const TAG_RE = /^[A-Za-z0-9._-]{1,32}$/;
18
+ const REQUEST_TIMEOUT_MS = 4000;
19
+
20
+ function manualRoute() {
21
+ return [
22
+ 'Manual route:',
23
+ ` git clone https://github.com/${OWNER}/${REPO}.git`,
24
+ ' (or click "Use this template" on GitHub, or grab the zip from Releases)',
25
+ ].join('\n');
26
+ }
27
+
28
+ function fail(message) {
29
+ console.error(`\nlararium: ${message}\n`);
30
+ console.error(manualRoute());
31
+ process.exit(1);
32
+ }
33
+
34
+ // GET a URL, following redirects, either buffering text or streaming to destFile.
35
+ function httpGet(url, { asJson = false, destFile = null } = {}, redirectsLeft = 5) {
36
+ return new Promise((resolve, reject) => {
37
+ const req = https.get(
38
+ url,
39
+ { headers: { 'User-Agent': USER_AGENT, Accept: asJson ? 'application/vnd.github+json' : '*/*' } },
40
+ (res) => {
41
+ if ([301, 302, 303, 307, 308].includes(res.statusCode) && res.headers.location) {
42
+ res.resume();
43
+ if (redirectsLeft <= 0) {
44
+ reject(new Error('too many redirects'));
45
+ return;
46
+ }
47
+ resolve(httpGet(res.headers.location, { asJson, destFile }, redirectsLeft - 1));
48
+ return;
49
+ }
50
+ if (res.statusCode < 200 || res.statusCode >= 300) {
51
+ res.resume();
52
+ reject(new Error(`HTTP ${res.statusCode} for ${url}`));
53
+ return;
54
+ }
55
+ if (destFile) {
56
+ const out = fs.createWriteStream(destFile);
57
+ res.pipe(out);
58
+ out.on('finish', () => out.close(() => resolve({ statusCode: res.statusCode })));
59
+ out.on('error', reject);
60
+ } else {
61
+ let data = '';
62
+ res.setEncoding('utf8');
63
+ res.on('data', (chunk) => {
64
+ data += chunk;
65
+ });
66
+ res.on('end', () => resolve({ statusCode: res.statusCode, body: data }));
67
+ }
68
+ }
69
+ );
70
+ req.on('error', reject);
71
+ req.setTimeout(REQUEST_TIMEOUT_MS, () => {
72
+ req.destroy(new Error('request timed out'));
73
+ });
74
+ });
75
+ }
76
+
77
+ async function resolveRef() {
78
+ const apiUrl = `https://api.github.com/repos/${OWNER}/${REPO}/releases/latest`;
79
+ try {
80
+ const res = await httpGet(apiUrl, { asJson: true });
81
+ const parsed = JSON.parse(res.body);
82
+ const tag = parsed && parsed.tag_name;
83
+ if (typeof tag === 'string' && TAG_RE.test(tag)) {
84
+ return { ref: tag, kind: 'tags', usedFallback: false };
85
+ }
86
+ throw new Error('releases API returned no usable tag');
87
+ } catch (err) {
88
+ console.log(`lararium: could not resolve the latest release (${err.message}). Falling back to main.`);
89
+ return { ref: 'main', kind: 'heads', usedFallback: true };
90
+ }
91
+ }
92
+
93
+ async function downloadTarball(ref, kind) {
94
+ const url = `https://codeload.github.com/${OWNER}/${REPO}/tar.gz/refs/${kind}/${ref}`;
95
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'lararium-'));
96
+ const tarPath = path.join(tmpDir, 'stack.tar.gz');
97
+ await httpGet(url, { destFile: tarPath });
98
+ const stat = fs.statSync(tarPath);
99
+ if (!stat.size) {
100
+ throw new Error('downloaded tarball is empty');
101
+ }
102
+ return tarPath;
103
+ }
104
+
105
+ async function run() {
106
+ const folderArg = process.argv[2] || 'lararium';
107
+ const target = path.resolve(process.cwd(), folderArg);
108
+
109
+ if (fs.existsSync(target)) {
110
+ const stat = fs.statSync(target);
111
+ if (!stat.isDirectory()) {
112
+ fail(`"${folderArg}" exists and is not a directory.`);
113
+ }
114
+ if (fs.readdirSync(target).length > 0) {
115
+ fail(`"${folderArg}" already exists and is not empty. Pick a different folder name or clear it first.`);
116
+ }
117
+ } else {
118
+ fs.mkdirSync(target, { recursive: true });
119
+ }
120
+
121
+ console.log('lararium: resolving the latest release...');
122
+ const { ref, kind, usedFallback } = await resolveRef();
123
+
124
+ console.log(`lararium: downloading ${ref}...`);
125
+ let tarPath;
126
+ try {
127
+ tarPath = await downloadTarball(ref, kind);
128
+ } catch (err) {
129
+ fail(
130
+ `could not download the template (${err.message}). ` +
131
+ 'This is expected if the repo is not public yet, or a network issue.'
132
+ );
133
+ return;
134
+ }
135
+
136
+ console.log(`lararium: unpacking into ${target}...`);
137
+ try {
138
+ execFileSync('tar', ['-xzf', tarPath, '-C', target, '--strip-components=1']);
139
+ } catch (err) {
140
+ fail(`tar extraction failed (${err.message}). Your system may be missing tar.`);
141
+ return;
142
+ }
143
+
144
+ // The scaffolder's own package dir doesn't belong in the recipient's copy,
145
+ // and neither do this repo's own CI files, if any ship in the release.
146
+ for (const dir of ['npx', '.github']) {
147
+ const p = path.join(target, dir);
148
+ if (fs.existsSync(p)) {
149
+ fs.rmSync(p, { recursive: true, force: true });
150
+ }
151
+ }
152
+
153
+ console.log('lararium: initializing git...');
154
+ try {
155
+ execFileSync('git', ['init'], { cwd: target, stdio: 'ignore' });
156
+ execFileSync('git', ['add', '-A'], { cwd: target, stdio: 'ignore' });
157
+ execFileSync('git', ['commit', '-m', 'Initial commit from lararium template'], {
158
+ cwd: target,
159
+ stdio: 'ignore',
160
+ });
161
+ } catch (err) {
162
+ console.log(
163
+ 'lararium: git init/commit skipped (configure git user.name and user.email, then commit by hand).'
164
+ );
165
+ }
166
+
167
+ console.log('');
168
+ console.log(`Landed in ${target}, fetched ${usedFallback ? 'main (no release found)' : ref}.`);
169
+ console.log('');
170
+ console.log(` cd ${folderArg}`);
171
+ console.log(' claude');
172
+ console.log('');
173
+ console.log('Then say: "Run the install interview in INSTALL.md".');
174
+ console.log('');
175
+ }
176
+
177
+ run().catch((err) => {
178
+ fail(err && err.message ? err.message : String(err));
179
+ });
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "lararium",
3
+ "version": "0.1.0",
4
+ "description": "One-line scaffolder for the lararium template: fetches the latest release and unpacks it into a new folder.",
5
+ "bin": {
6
+ "lararium": "./index.js"
7
+ },
8
+ "files": [
9
+ "index.js",
10
+ "README.md"
11
+ ],
12
+ "engines": {
13
+ "node": ">=18"
14
+ },
15
+ "license": "MIT",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "https://github.com/kylnor/lararium.git"
19
+ },
20
+ "keywords": [
21
+ "scaffold",
22
+ "template",
23
+ "claude-code",
24
+ "agentic"
25
+ ]
26
+ }