dwellentix-installer 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/README.md +76 -0
- package/bin/dwellentix-install.js +208 -0
- package/package.json +17 -0
package/README.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# @dwellentix/installer
|
|
2
|
+
|
|
3
|
+
Script executor and maintainer for Dwellentix private install workflows.
|
|
4
|
+
|
|
5
|
+
It is not a package manager. It keeps package-server auth out of npm's remote fetch path, then lets npm install from a local tarball:
|
|
6
|
+
|
|
7
|
+
1. Reads `.env`.
|
|
8
|
+
2. Reads `dwellentix.json`.
|
|
9
|
+
3. Downloads protected `.tgz` files with configured auth.
|
|
10
|
+
4. Executes `npm install --no-save <downloaded.tgz>`.
|
|
11
|
+
|
|
12
|
+
## Scope
|
|
13
|
+
|
|
14
|
+
This repo owns the install workflow script:
|
|
15
|
+
|
|
16
|
+
- config parsing
|
|
17
|
+
- `.env` loading
|
|
18
|
+
- private tarball download
|
|
19
|
+
- auth header construction
|
|
20
|
+
- optional checksum validation
|
|
21
|
+
- execution of npm with local tarballs
|
|
22
|
+
|
|
23
|
+
It does not manually copy files into `node_modules`, resolve dependency graphs, or replace npm.
|
|
24
|
+
|
|
25
|
+
## Config
|
|
26
|
+
|
|
27
|
+
Create `dwellentix.json` in the consuming project:
|
|
28
|
+
|
|
29
|
+
```json
|
|
30
|
+
{
|
|
31
|
+
"cacheDir": ".dwellentix-cache",
|
|
32
|
+
"npmArgs": ["install", "--no-save"],
|
|
33
|
+
"packages": [
|
|
34
|
+
{
|
|
35
|
+
"name": "@dwellentix/lib",
|
|
36
|
+
"version": "2.1.1",
|
|
37
|
+
"url": "https://dwellentix-lib-artifactory.onrender.com/packages/dwellentix-lib-2.1.1.tgz",
|
|
38
|
+
"tokenEnv": "DWELLENTIX_LIB_TOKEN"
|
|
39
|
+
}
|
|
40
|
+
]
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The default auth mode is:
|
|
45
|
+
|
|
46
|
+
```txt
|
|
47
|
+
Authorization: Bearer <tokenEnv value>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Usage
|
|
51
|
+
|
|
52
|
+
From a consuming project:
|
|
53
|
+
|
|
54
|
+
```powershell
|
|
55
|
+
node ..\dwellentix-installer\bin\dwellentix-install.js install
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Or add a package script:
|
|
59
|
+
|
|
60
|
+
```json
|
|
61
|
+
{
|
|
62
|
+
"scripts": {
|
|
63
|
+
"install:private": "node ../dwellentix-installer/bin/dwellentix-install.js install"
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Then run:
|
|
69
|
+
|
|
70
|
+
```powershell
|
|
71
|
+
npm.cmd run install:private
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Notes
|
|
75
|
+
|
|
76
|
+
Do not manually copy files into `node_modules`. This executor only handles the authenticated download and command orchestration; npm still handles package extraction and linking from the local tarball.
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createHash } from 'node:crypto'
|
|
3
|
+
import { createWriteStream, existsSync, mkdirSync, readFileSync, rmSync } from 'node:fs'
|
|
4
|
+
import { request } from 'node:http'
|
|
5
|
+
import { request as requestHttps } from 'node:https'
|
|
6
|
+
import { basename, resolve } from 'node:path'
|
|
7
|
+
import { spawnSync } from 'node:child_process'
|
|
8
|
+
|
|
9
|
+
const command = process.argv[2]
|
|
10
|
+
const flags = parseFlags(process.argv.slice(3))
|
|
11
|
+
const cwd = process.cwd()
|
|
12
|
+
const configPath = resolve(cwd, flags.config || 'dwellentix.json')
|
|
13
|
+
const envPath = resolve(cwd, flags.env || '.env')
|
|
14
|
+
|
|
15
|
+
if (command !== 'install') {
|
|
16
|
+
console.error('Usage: dwellentix-install install [--config dwellentix.json] [--env .env]')
|
|
17
|
+
process.exit(1)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
loadEnv(envPath)
|
|
21
|
+
|
|
22
|
+
const config = readJson(configPath)
|
|
23
|
+
const packages = Array.isArray(config.packages) ? config.packages : []
|
|
24
|
+
|
|
25
|
+
if (packages.length === 0) {
|
|
26
|
+
console.error(`No packages configured in ${configPath}`)
|
|
27
|
+
process.exit(1)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const cacheDir = resolve(cwd, config.cacheDir || '.dwellentix-cache')
|
|
31
|
+
mkdirSync(cacheDir, { recursive: true })
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const downloaded = []
|
|
35
|
+
|
|
36
|
+
for (const pkg of packages) {
|
|
37
|
+
downloaded.push(await downloadPackage(pkg, cacheDir))
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
installTarballs(downloaded, config)
|
|
41
|
+
} catch (error) {
|
|
42
|
+
console.error(`[dwellentix-install] ${(error && error.message) || error}`)
|
|
43
|
+
process.exit(1)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function parseFlags(args) {
|
|
47
|
+
const result = {}
|
|
48
|
+
|
|
49
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
50
|
+
const arg = args[index]
|
|
51
|
+
if (!arg.startsWith('--')) continue
|
|
52
|
+
|
|
53
|
+
const [rawKey, inlineValue] = arg.slice(2).split('=', 2)
|
|
54
|
+
result[rawKey] = inlineValue ?? args[index + 1]
|
|
55
|
+
if (inlineValue === undefined) index += 1
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return result
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function readJson(filePath) {
|
|
62
|
+
try {
|
|
63
|
+
return JSON.parse(readFileSync(filePath, 'utf8'))
|
|
64
|
+
} catch (error) {
|
|
65
|
+
console.error(`Unable to read ${filePath}: ${(error && error.message) || error}`)
|
|
66
|
+
process.exit(1)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function loadEnv(filePath) {
|
|
71
|
+
if (!existsSync(filePath)) return
|
|
72
|
+
|
|
73
|
+
const lines = readFileSync(filePath, 'utf8').split(/\r?\n/)
|
|
74
|
+
|
|
75
|
+
for (const line of lines) {
|
|
76
|
+
const trimmed = line.trim()
|
|
77
|
+
if (!trimmed || trimmed.startsWith('#')) continue
|
|
78
|
+
|
|
79
|
+
const separatorIndex = trimmed.indexOf('=')
|
|
80
|
+
if (separatorIndex === -1) continue
|
|
81
|
+
|
|
82
|
+
const key = trimmed.slice(0, separatorIndex).trim()
|
|
83
|
+
const value = unquote(trimmed.slice(separatorIndex + 1))
|
|
84
|
+
|
|
85
|
+
if (key && process.env[key] === undefined) {
|
|
86
|
+
process.env[key] = value
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function unquote(value) {
|
|
92
|
+
const trimmed = value.trim()
|
|
93
|
+
const quote = trimmed[0]
|
|
94
|
+
return (quote === '"' || quote === "'") && trimmed.endsWith(quote)
|
|
95
|
+
? trimmed.slice(1, -1)
|
|
96
|
+
: trimmed
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function downloadPackage(pkg, cacheDir) {
|
|
100
|
+
if (!pkg || typeof pkg !== 'object') {
|
|
101
|
+
throw new Error('Package entries must be objects')
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (!pkg.name || !pkg.url) {
|
|
105
|
+
throw new Error('Package entries require name and url')
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const url = new URL(pkg.url)
|
|
109
|
+
const fileName = pkg.fileName || basename(url.pathname)
|
|
110
|
+
const destination = resolve(cacheDir, fileName)
|
|
111
|
+
const headers = buildHeaders(pkg)
|
|
112
|
+
|
|
113
|
+
console.log(`[dwellentix-install] Downloading ${pkg.name}@${pkg.version || fileName}`)
|
|
114
|
+
await download(url, destination, headers)
|
|
115
|
+
|
|
116
|
+
if (pkg.sha512) {
|
|
117
|
+
verifySha512(destination, pkg.sha512, pkg.name)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return destination
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function buildHeaders(pkg) {
|
|
124
|
+
const headers = { ...(pkg.headers || {}) }
|
|
125
|
+
const token = pkg.tokenEnv ? process.env[pkg.tokenEnv] : undefined
|
|
126
|
+
|
|
127
|
+
if (pkg.auth === false) return headers
|
|
128
|
+
|
|
129
|
+
if (pkg.auth?.type === 'basic') {
|
|
130
|
+
const username = valueFromEnvOrLiteral(pkg.auth.usernameEnv, pkg.auth.username)
|
|
131
|
+
const password = valueFromEnvOrLiteral(pkg.auth.passwordEnv, pkg.auth.password)
|
|
132
|
+
if (!username || !password) throw new Error(`Missing basic auth credentials for ${pkg.name}`)
|
|
133
|
+
headers.authorization = `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`
|
|
134
|
+
return headers
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (pkg.auth?.type === 'header') {
|
|
138
|
+
const value = valueFromEnvOrLiteral(pkg.auth.valueEnv, pkg.auth.value)
|
|
139
|
+
if (!pkg.auth.name || !value) throw new Error(`Missing header auth config for ${pkg.name}`)
|
|
140
|
+
headers[pkg.auth.name] = value
|
|
141
|
+
return headers
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (!token) throw new Error(`Missing ${pkg.tokenEnv || 'tokenEnv'} for ${pkg.name}`)
|
|
145
|
+
headers.authorization = `Bearer ${token}`
|
|
146
|
+
return headers
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function valueFromEnvOrLiteral(envName, literal) {
|
|
150
|
+
return envName ? process.env[envName] : literal
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function download(url, destination, headers) {
|
|
154
|
+
return new Promise((resolveDownload, rejectDownload) => {
|
|
155
|
+
const client = url.protocol === 'https:' ? requestHttps : request
|
|
156
|
+
const req = client(url, { headers }, (res) => {
|
|
157
|
+
if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
158
|
+
const redirectedUrl = new URL(res.headers.location, url)
|
|
159
|
+
res.resume()
|
|
160
|
+
download(redirectedUrl, destination, headers).then(resolveDownload, rejectDownload)
|
|
161
|
+
return
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (res.statusCode !== 200) {
|
|
165
|
+
let body = ''
|
|
166
|
+
res.setEncoding('utf8')
|
|
167
|
+
res.on('data', (chunk) => { body += chunk })
|
|
168
|
+
res.on('end', () => rejectDownload(new Error(`Download failed with HTTP ${res.statusCode}: ${body}`)))
|
|
169
|
+
return
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const file = createWriteStream(destination)
|
|
173
|
+
res.pipe(file)
|
|
174
|
+
file.on('finish', () => file.close(resolveDownload))
|
|
175
|
+
file.on('error', (error) => {
|
|
176
|
+
rmSync(destination, { force: true })
|
|
177
|
+
rejectDownload(error)
|
|
178
|
+
})
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
req.on('error', rejectDownload)
|
|
182
|
+
req.end()
|
|
183
|
+
})
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function verifySha512(filePath, expected, packageName) {
|
|
187
|
+
const hash = createHash('sha512').update(readFileSync(filePath)).digest('base64')
|
|
188
|
+
const normalizedExpected = expected.startsWith('sha512-') ? expected.slice('sha512-'.length) : expected
|
|
189
|
+
|
|
190
|
+
if (hash !== normalizedExpected) {
|
|
191
|
+
throw new Error(`sha512 mismatch for ${packageName}`)
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function installTarballs(tarballs, config) {
|
|
196
|
+
const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm'
|
|
197
|
+
const installArgs = Array.isArray(config.npmArgs) ? config.npmArgs : ['install', '--no-save']
|
|
198
|
+
const args = [...installArgs, ...tarballs]
|
|
199
|
+
|
|
200
|
+
console.log(`[dwellentix-install] Running ${npmCommand} ${args.join(' ')}`)
|
|
201
|
+
const result = spawnSync(npmCommand, args, {
|
|
202
|
+
cwd,
|
|
203
|
+
env: process.env,
|
|
204
|
+
stdio: 'inherit',
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
process.exit(result.status ?? 1)
|
|
208
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dwellentix-installer",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Script executor for Dwellentix private package install workflows.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"dwellentix-install": "./bin/dwellentix-install.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"install:private": "node bin/dwellentix-install.js install",
|
|
11
|
+
"update:private": "node bin/dwellentix-install.js update"
|
|
12
|
+
},
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=18.0.0"
|
|
15
|
+
},
|
|
16
|
+
"license": "UNLICENSED"
|
|
17
|
+
}
|