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 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
+ }