dshm-compat 0.1.0 → 0.1.1
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/CHANGELOG.md +20 -0
- package/INSTALL.md +18 -7
- package/README.md +9 -3
- package/index.js +25 -5
- package/lib/local-install.js +155 -0
- package/package.json +2 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.1
|
|
4
|
+
|
|
5
|
+
- Tool results are now lossless JSON: `undefined`-valued fields are stripped at
|
|
6
|
+
the tool boundary. This fixes `compat_analyze` / `compat_plan` / `compat_scan`
|
|
7
|
+
failing with `value is not lossless JSON` on DSHM.
|
|
8
|
+
- Local tarball installation: `compat_install` (and `compat_convert` with
|
|
9
|
+
`install:true`) unpacks a `.tgz` into the owning DSH profile's `node_modules`,
|
|
10
|
+
records the dependency, and appends the bundle. This works around DSHM's host
|
|
11
|
+
installer rejecting local paths, `file:` and `http://` specs. A restart is
|
|
12
|
+
required, and the result says `restartRequired: true`.
|
|
13
|
+
- Local install preserves the tarball's file modes (needed for native worker
|
|
14
|
+
binaries such as `dsh-ops-worker`).
|
|
15
|
+
- Docs updated with the DSHM `pnpm view` limitation and the local install path.
|
|
16
|
+
|
|
17
|
+
## 0.1.0
|
|
18
|
+
|
|
19
|
+
- Initial analyzer, S0/S1 converter, dependency preflight/vendoring, guest
|
|
20
|
+
probe/provision, install queue, and dsh-ops 0.3.x recipe.
|
package/INSTALL.md
CHANGED
|
@@ -5,15 +5,26 @@ It is host-only, has no runtime dependencies, and adds eight agent tools.
|
|
|
5
5
|
|
|
6
6
|
## A. DSHM (the tablet)
|
|
7
7
|
|
|
8
|
-
### A1. npm or GitHub
|
|
8
|
+
### A1. npm or GitHub
|
|
9
9
|
|
|
10
10
|
Publish the package to npm (remove `"private": true` from `package.json`
|
|
11
|
-
first), or push this directory to a GitHub repository
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
first), or push this directory to a GitHub repository.
|
|
12
|
+
|
|
13
|
+
On DSHM builds without a shell, the official **Settings → Plugins → Add plugin**
|
|
14
|
+
input calls `pnpm view` before installing and fails with
|
|
15
|
+
`Command failed with EACCES: pnpm view ... spawn pnpm EACCES`. Use one of these
|
|
16
|
+
instead:
|
|
17
|
+
|
|
18
|
+
- ask the DSHM Agent to run `pnpm add dshm-compat@0.1.1` in its bash tool (the
|
|
19
|
+
bundled pnpm shim routes that command to the host install queue); or
|
|
20
|
+
- install from a GitHub repository URL; or
|
|
21
|
+
- after any working install, use the plugin's own `compat_install` tool for
|
|
22
|
+
npm/GitHub specs.
|
|
23
|
+
|
|
24
|
+
The plugin's local converted tarballs are installed by `compat_install` itself:
|
|
25
|
+
it unpacks them into `profiles/<profile>/node_modules/<name>`, records the
|
|
26
|
+
dependency and bundle, and asks for a DSHM restart. The host install queue does
|
|
27
|
+
not accept local paths, `file:` or `http://` specs.
|
|
17
28
|
|
|
18
29
|
### A2. HTTPS tarball
|
|
19
30
|
|
package/README.md
CHANGED
|
@@ -5,8 +5,10 @@ them through the least invasive strategy that can work.
|
|
|
5
5
|
|
|
6
6
|
## Current phase
|
|
7
7
|
|
|
8
|
+
0.1.1 fixes DSHM tool-result validation and adds local-tarball installation.
|
|
8
9
|
Phase 1 ships the analyzer, the S0/S1 converter, the dsh-ops recipe, guest
|
|
9
|
-
probing/provisioning,
|
|
10
|
+
probing/provisioning, the DSHM install-queue transport, and a profile-local
|
|
11
|
+
fallback for artifacts the host queue rejects:
|
|
10
12
|
|
|
11
13
|
- `compat_analyze` downloads an npm/GitHub/local package and classifies it.
|
|
12
14
|
- `compat_plan` returns the concrete conversion plan without changing anything.
|
|
@@ -22,8 +24,12 @@ probing/provisioning, and the DSHM install-queue transport:
|
|
|
22
24
|
- `compat_guest_provision` installs a reviewed allowlist of Alpine packages in
|
|
23
25
|
the guest (`confirm:true` required).
|
|
24
26
|
- `compat_status` lists stored reports and install attempts.
|
|
25
|
-
- `compat_install`
|
|
26
|
-
(`
|
|
27
|
+
- `compat_install` installs an npm/GitHub spec through the host queue. Local
|
|
28
|
+
`.tgz` paths (and `file:`/`link:`) are unpacked directly into the owning DSH
|
|
29
|
+
profile and its bundle list, then need a DSHM restart; this is required
|
|
30
|
+
because the DSHM host queue rejects local specs.
|
|
31
|
+
- `compat_convert` with `install:true` uses the same local fallback for its
|
|
32
|
+
converted tarball.
|
|
27
33
|
|
|
28
34
|
The converter deliberately stops at the manifest/repack/vendor boundary:
|
|
29
35
|
source transpilation and native builds are later phases. A report saying
|
package/index.js
CHANGED
|
@@ -12,6 +12,7 @@ import { scan } from './lib/batch.js'
|
|
|
12
12
|
import { convert } from './lib/convert.js'
|
|
13
13
|
import { probeGuest, provisionGuest } from './lib/guest.js'
|
|
14
14
|
import { enqueueInstall } from './lib/install.js'
|
|
15
|
+
import { installLocalTarball, isLocalSpec } from './lib/local-install.js'
|
|
15
16
|
import { buildPlan } from './lib/plan.js'
|
|
16
17
|
import { recordInstall, recordReport, status } from './lib/state.js'
|
|
17
18
|
|
|
@@ -47,7 +48,13 @@ function definition(name, description, parameters, execute) {
|
|
|
47
48
|
description: description + RESULT_NOTE,
|
|
48
49
|
parameters,
|
|
49
50
|
output: { schema: OUTPUT, render: renderJson },
|
|
50
|
-
|
|
51
|
+
// DSH validates tool results as lossless JSON; undefined-valued fields are
|
|
52
|
+
// rejected. Strip them once at the boundary so every tool is safe.
|
|
53
|
+
execute: async (args, exec) => {
|
|
54
|
+
const value = await execute(args, exec)
|
|
55
|
+
if (value === undefined) return null
|
|
56
|
+
try { return JSON.parse(JSON.stringify(value)) } catch (error) { return errorResult(error) }
|
|
57
|
+
},
|
|
51
58
|
}
|
|
52
59
|
}
|
|
53
60
|
|
|
@@ -145,6 +152,7 @@ export function apply(ctx) {
|
|
|
145
152
|
confirm: { type: 'boolean', description: 'Required when install is true. Confirms that installing the converted package runs third-party code.' },
|
|
146
153
|
vendor: { type: 'boolean', description: 'Vendor missing pure-JS runtime dependencies into the tarball. Default true.' },
|
|
147
154
|
workerPath: { type: 'string', description: 'Absolute path to a prebuilt AArch64 Linux worker for recipes that need one (currently dsh-ops). Omit to receive the build command instead.' },
|
|
155
|
+
allowLocal: { type: 'boolean', description: 'When install is true, unpack the converted tarball into the profile instead of the host queue. Default true; set false to force the queue.' },
|
|
148
156
|
},
|
|
149
157
|
required: ['spec'],
|
|
150
158
|
additionalProperties: false,
|
|
@@ -160,7 +168,10 @@ export function apply(ctx) {
|
|
|
160
168
|
if (args.confirm !== true) {
|
|
161
169
|
return { ...result, install: { status: 'confirmation-required', message: 'Re-run with install:true and confirm:true to submit the tarball.' } }
|
|
162
170
|
}
|
|
163
|
-
const
|
|
171
|
+
const local = args.allowLocal === false ? undefined : installLocalTarball(result.tarball, { profileDir: args.profileDir })
|
|
172
|
+
const installed = local === undefined
|
|
173
|
+
? await enqueueInstall(`file:${result.tarball}`, { profileDir: args.profileDir, signal: exec?.signal })
|
|
174
|
+
: { status: local.ok ? 'ok' : 'error', ...local }
|
|
164
175
|
recordInstall({ spec: args.spec, tarball: result.tarball, at: new Date().toISOString(), ...installed })
|
|
165
176
|
return { ...result, install: installed }
|
|
166
177
|
} catch (error) {
|
|
@@ -213,13 +224,14 @@ export function apply(ctx) {
|
|
|
213
224
|
|
|
214
225
|
register(definition(
|
|
215
226
|
'compat_install',
|
|
216
|
-
'
|
|
217
|
-
+ '
|
|
227
|
+
'Install an already-built plugin artifact. Local .tgz paths (or file:/link: paths) are unpacked into the owning DSH profile and its bundle list, because DSHM\'s host queue rejects local specs; npm names and GitHub repositories still go through the host install queue. '
|
|
228
|
+
+ 'Requires confirm:true because installing a plugin runs third-party code; a local install needs a DSHM restart to load.',
|
|
218
229
|
{
|
|
219
230
|
type: 'object',
|
|
220
231
|
properties: {
|
|
221
232
|
spec: { type: 'string', description: 'A file:/absolute-path .tgz produced by the converter, or an npm/GitHub spec.' },
|
|
222
|
-
profileDir: { type: 'string', description: 'Absolute DSHM profile directory. Omit to
|
|
233
|
+
profileDir: { type: 'string', description: 'Absolute DSHM profile directory. Omit to infer it from this plugin\'s installation path.' },
|
|
234
|
+
allowLocal: { type: 'boolean', description: 'Allow unpacking a local tarball into the profile. Default true. Set false to force the host install queue.' },
|
|
223
235
|
confirm: { type: 'boolean', description: 'Must be true. The caller confirms that installing this package runs third-party code.' },
|
|
224
236
|
},
|
|
225
237
|
required: ['spec', 'confirm'],
|
|
@@ -230,6 +242,14 @@ export function apply(ctx) {
|
|
|
230
242
|
return { status: 'confirmation-required', message: 'Re-run with confirm:true after reviewing the analyzer report.' }
|
|
231
243
|
}
|
|
232
244
|
try {
|
|
245
|
+
if (args.allowLocal !== false && isLocalSpec(args.spec)) {
|
|
246
|
+
const path = args.spec.trim().replace(/^(?:file|link):/i, '')
|
|
247
|
+
const local = installLocalTarball(path, { profileDir: args.profileDir })
|
|
248
|
+
recordInstall({ spec: args.spec, at: new Date().toISOString(), ...local })
|
|
249
|
+
return local.ok
|
|
250
|
+
? { status: 'ok', ...local }
|
|
251
|
+
: { status: 'error', error: { code: 'LOCAL_INSTALL_FAILED', message: local.error ?? 'local install failed' }, ...local }
|
|
252
|
+
}
|
|
233
253
|
const result = await enqueueInstall(args.spec, { profileDir: args.profileDir, signal: exec?.signal })
|
|
234
254
|
recordInstall({ spec: args.spec, at: new Date().toISOString(), ...result })
|
|
235
255
|
return result
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manual profile installation for converted artifacts.
|
|
3
|
+
*
|
|
4
|
+
* DSHM's host installer accepts npm names and GitHub repositories, but rejects
|
|
5
|
+
* local tarballs, `file:` and `http://` specs. A converted package has to land
|
|
6
|
+
* in the profile another way: unpack it under `<profile>/node_modules/<name>`,
|
|
7
|
+
* record the dependency, and append the bundle so the next boot composes its
|
|
8
|
+
* patch. This mirrors the manual installation the DSHM agent performed for
|
|
9
|
+
* `dshm-compat` itself.
|
|
10
|
+
*
|
|
11
|
+
* @module dshm-compat/local-install
|
|
12
|
+
*/
|
|
13
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs'
|
|
14
|
+
import { basename, dirname, isAbsolute, join, resolve } from 'node:path'
|
|
15
|
+
import { fileURLToPath } from 'node:url'
|
|
16
|
+
import { dshHome } from './state.js'
|
|
17
|
+
import { extractTar } from './tar.js'
|
|
18
|
+
import { validateTarball } from './validate.js'
|
|
19
|
+
|
|
20
|
+
/** @param {unknown} value */
|
|
21
|
+
function record(value) {
|
|
22
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value) ? value : undefined
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** @param {string} file */
|
|
26
|
+
function readJson(file) {
|
|
27
|
+
return JSON.parse(readFileSync(file, 'utf8').replace(/^\uFEFF/, ''))
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** @param {string} file @param {unknown} value */
|
|
31
|
+
function writeJsonAtomic(file, value) {
|
|
32
|
+
const text = `${JSON.stringify(value, null, 2)}\n`
|
|
33
|
+
const temporary = `${file}.tmp-${process.pid}-${Date.now()}`
|
|
34
|
+
writeFileSync(temporary, text, 'utf8')
|
|
35
|
+
try { renameSync(temporary, file) } catch {
|
|
36
|
+
writeFileSync(file, text, 'utf8')
|
|
37
|
+
try { rmSync(temporary, { force: true }) } catch { /* best effort */ }
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** @param {string} candidate */
|
|
42
|
+
function looksLikeProfile(candidate) {
|
|
43
|
+
return existsSync(join(candidate, 'package.json'))
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** @param {NodeJS.ProcessEnv} env */
|
|
47
|
+
function profileFromModule() {
|
|
48
|
+
let current = dirname(fileURLToPath(import.meta.url))
|
|
49
|
+
for (let depth = 0; depth < 8; depth += 1) {
|
|
50
|
+
if (basename(current) === 'node_modules') return dirname(current)
|
|
51
|
+
const parent = dirname(current)
|
|
52
|
+
if (parent === current) break
|
|
53
|
+
current = parent
|
|
54
|
+
}
|
|
55
|
+
return undefined
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Resolve the profile that owns this plugin.
|
|
60
|
+
* @param {NodeJS.ProcessEnv} env
|
|
61
|
+
* @returns {string | undefined}
|
|
62
|
+
*/
|
|
63
|
+
export function resolveProfileDir(env = process.env) {
|
|
64
|
+
if (typeof env.DSH_PROFILE_DIR === 'string' && looksLikeProfile(env.DSH_PROFILE_DIR)) return env.DSH_PROFILE_DIR
|
|
65
|
+
const fromModule = profileFromModule()
|
|
66
|
+
if (fromModule !== undefined && looksLikeProfile(fromModule)) return fromModule
|
|
67
|
+
const name = env.DSH_PROFILE
|
|
68
|
+
if (typeof name === 'string' && name !== '') {
|
|
69
|
+
const candidate = join(dshHome(env), 'profiles', name)
|
|
70
|
+
if (looksLikeProfile(candidate)) return candidate
|
|
71
|
+
}
|
|
72
|
+
return undefined
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** @param {string} spec */
|
|
76
|
+
export function isLocalSpec(spec) {
|
|
77
|
+
if (typeof spec !== 'string' || spec.trim() === '') return false
|
|
78
|
+
const raw = spec.trim()
|
|
79
|
+
if (/^(?:file|link):/i.test(raw)) return true
|
|
80
|
+
if (isAbsolute(raw)) return true
|
|
81
|
+
return /\.(?:tgz|tar\.gz)(?:[?#].*)?$/i.test(raw)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** @param {string} target @param {import('./tar.js').TarEntry[]} entries */
|
|
85
|
+
function writeEntries(target, entries) {
|
|
86
|
+
rmSync(target, { recursive: true, force: true })
|
|
87
|
+
mkdirSync(target, { recursive: true })
|
|
88
|
+
for (const entry of entries) {
|
|
89
|
+
if (entry.type !== 'file') continue
|
|
90
|
+
const relative = entry.path.replace(/^package\//, '')
|
|
91
|
+
if (relative === '' || relative.startsWith('/') || relative.split('/').includes('..')) continue
|
|
92
|
+
const file = join(target, relative)
|
|
93
|
+
mkdirSync(dirname(file), { recursive: true })
|
|
94
|
+
writeFileSync(file, entry.data, { mode: entry.mode === 0 ? 0o644 : entry.mode & 0o777 })
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* @param {string} tarball
|
|
100
|
+
* @param {{ profileDir?: string, env?: NodeJS.ProcessEnv }} [options]
|
|
101
|
+
*/
|
|
102
|
+
export function installLocalTarball(tarball, options = {}) {
|
|
103
|
+
const env = options.env ?? process.env
|
|
104
|
+
if (typeof tarball !== 'string' || !existsSync(tarball)) {
|
|
105
|
+
return { ok: false, error: `tarball not found: ${String(tarball)}` }
|
|
106
|
+
}
|
|
107
|
+
const validation = validateTarball(tarball)
|
|
108
|
+
if (!validation.ok) return { ok: false, error: `converted tarball is invalid: ${validation.errors.join('; ')}` }
|
|
109
|
+
|
|
110
|
+
const profileDir = typeof options.profileDir === 'string' && options.profileDir !== ''
|
|
111
|
+
? resolve(options.profileDir)
|
|
112
|
+
: resolveProfileDir(env)
|
|
113
|
+
if (profileDir === undefined || !looksLikeProfile(profileDir)) {
|
|
114
|
+
return { ok: false, error: 'cannot locate the DSH profile that owns this plugin; pass profileDir explicitly' }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const { entries } = extractTar(readFileSync(tarball))
|
|
118
|
+
const packageEntry = entries.find(entry => entry.path === 'package/package.json')
|
|
119
|
+
if (packageEntry?.data === undefined) return { ok: false, error: 'converted tarball has no package/package.json' }
|
|
120
|
+
const pkg = record(JSON.parse(packageEntry.data.toString('utf8')))
|
|
121
|
+
const name = typeof pkg?.name === 'string' ? pkg.name : undefined
|
|
122
|
+
const version = typeof pkg?.version === 'string' ? pkg.version : '0.0.0'
|
|
123
|
+
if (name === undefined || !/^(?:@[a-z0-9][a-z0-9._~-]*\/)?[a-z0-9][a-z0-9._~-]*$/i.test(name)) {
|
|
124
|
+
return { ok: false, error: 'converted tarball has no valid package name' }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const destination = join(profileDir, 'node_modules', ...name.split('/'))
|
|
128
|
+
writeEntries(destination, entries)
|
|
129
|
+
|
|
130
|
+
const manifestPath = join(profileDir, 'package.json')
|
|
131
|
+
const manifest = readJson(manifestPath)
|
|
132
|
+
if (record(manifest) === undefined) return { ok: false, error: `profile manifest is not an object: ${manifestPath}` }
|
|
133
|
+
const dependencies = record(manifest.dependencies) ?? {}
|
|
134
|
+
dependencies[name] = version
|
|
135
|
+
manifest.dependencies = dependencies
|
|
136
|
+
const dsh = record(manifest.dsh) ?? {}
|
|
137
|
+
const profile = record(dsh.profile) ?? {}
|
|
138
|
+
const bundles = Array.isArray(profile.bundles) ? profile.bundles.filter(item => typeof item === 'string') : []
|
|
139
|
+
if (!bundles.includes(name)) bundles.push(name)
|
|
140
|
+
profile.bundles = bundles
|
|
141
|
+
dsh.profile = profile
|
|
142
|
+
manifest.dsh = dsh
|
|
143
|
+
writeJsonAtomic(manifestPath, manifest)
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
ok: true,
|
|
147
|
+
method: 'local-profile',
|
|
148
|
+
profileDir,
|
|
149
|
+
name,
|
|
150
|
+
version,
|
|
151
|
+
destination,
|
|
152
|
+
restartRequired: true,
|
|
153
|
+
warnings: validation.warnings,
|
|
154
|
+
}
|
|
155
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dshm-compat",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Analyze and convert DSH plugins that cannot be installed or run on HarmonyOS DSHM.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"icon.svg",
|
|
30
30
|
"README.md",
|
|
31
31
|
"INSTALL.md",
|
|
32
|
+
"CHANGELOG.md",
|
|
32
33
|
"docs/*.md"
|
|
33
34
|
],
|
|
34
35
|
"engines": {
|