@le-space/rootfs 0.4.4 → 0.5.2

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/index.js CHANGED
@@ -222,7 +222,7 @@ function createRootfsBuildPlan(contract, options) {
222
222
  skipBuild: options.skipBuild === true,
223
223
  ipfsAddUrl: options.ipfsAddUrl?.trim() || "https://ipfs.aleph.cloud/api/v0/add",
224
224
  ipfsGatewayUrl: options.ipfsGatewayUrl?.trim() || "https://ipfs.aleph.cloud/ipfs",
225
- alephApiHost: options.alephApiHost?.trim() || "https://api2.aleph.im",
225
+ alephApiHost: options.alephApiHost?.trim() || "https://api.aleph.im",
226
226
  alephMessageWaitAttempts: positiveInteger(options.alephMessageWaitAttempts, 60),
227
227
  alephMessageWaitDelaySeconds: positiveInteger(options.alephMessageWaitDelaySeconds, 5),
228
228
  alephPinAttempts: positiveInteger(options.alephPinAttempts, 4),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@le-space/rootfs",
3
- "version": "0.4.4",
3
+ "version": "0.5.2",
4
4
  "description": "Shared rootfs contract parsing, reference profile assets, and build helpers.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -14,5 +14,13 @@
14
14
  },
15
15
  "publishConfig": {
16
16
  "access": "public"
17
+ },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "https://github.com/NiKrause/relay-button"
21
+ },
22
+ "homepage": "https://github.com/NiKrause/relay-button",
23
+ "bugs": {
24
+ "url": "https://github.com/NiKrause/relay-button/issues"
17
25
  }
18
26
  }
@@ -31,12 +31,12 @@ die() {
31
31
 
32
32
  load_rootfs_contract() {
33
33
  [ -n "${ROOTFS_CONTRACT_FILE}" ] || return 0
34
- require python3
34
+ require node
35
35
  [ -f "${ROOTFS_CONTRACT_FILE}" ] || die "Rootfs contract does not exist: ${ROOTFS_CONTRACT_FILE}"
36
36
 
37
37
  # The contract is relay-owned metadata. We load it once here so users can
38
38
  # select a standardized profile without repeating profile/install-mode flags.
39
- eval "$(python3 "${SCRIPT_DIR}/read-rootfs-contract.py" "${ROOTFS_CONTRACT_FILE}")"
39
+ eval "$(node "${SCRIPT_DIR}/read-rootfs-contract.mjs" "${ROOTFS_CONTRACT_FILE}")"
40
40
 
41
41
  if [ -n "${ROOTFS_PROFILE}" ] && [ "${ROOTFS_PROFILE}" != "${ROOTFS_CONTRACT_PROFILE}" ]; then
42
42
  die "ROOTFS_PROFILE=${ROOTFS_PROFILE} conflicts with contract profile ${ROOTFS_CONTRACT_PROFILE}"
@@ -119,25 +119,11 @@ resolve_rootfs_version() {
119
119
 
120
120
  if [ "${ROOTFS_PROFILE}" = "orbitdb-relay-pinner" ] && [ -n "${ORBITDB_RELAY_PINNER_DIR}" ]; then
121
121
  local orbitdb_package_json="${ORBITDB_RELAY_PINNER_DIR}/package.json"
122
- if [ -f "${orbitdb_package_json}" ] && command -v python3 >/dev/null 2>&1; then
122
+ if [ -f "${orbitdb_package_json}" ] && command -v node >/dev/null 2>&1; then
123
123
  local orbitdb_version
124
- orbitdb_version="$(python3 - "${orbitdb_package_json}" <<'PY'
125
- import json
126
- import sys
127
- from pathlib import Path
128
-
129
- try:
130
- payload = json.loads(Path(sys.argv[1]).read_text())
131
- except Exception:
132
- raise SystemExit(1)
133
-
134
- version = payload.get("version")
135
- if not isinstance(version, str) or not version.strip():
136
- raise SystemExit(1)
137
-
138
- print(f"orbitdb-relay-pinner-v{version.strip().lstrip('v')}")
139
- PY
140
- )" || orbitdb_version=""
124
+ orbitdb_version="$(
125
+ node -e "const fs=require('node:fs');const payload=JSON.parse(fs.readFileSync(process.argv[1],'utf8'));const version=typeof payload.version==='string'?payload.version.trim():'';if(!version)process.exit(1);console.log('orbitdb-relay-pinner-v'+version.replace(/^v/,''));" "${orbitdb_package_json}"
126
+ )" || orbitdb_version=""
141
127
 
142
128
  if [ -n "${orbitdb_version}" ]; then
143
129
  printf '%s\n' "${orbitdb_version}"
@@ -172,27 +158,6 @@ PY
172
158
 
173
159
  ROOTFS_VERSION="$(resolve_rootfs_version)"
174
160
 
175
- resolve_aleph_bin() {
176
- if [ -n "${ALEPH_BIN:-}" ]; then
177
- printf '%s\n' "${ALEPH_BIN}"
178
- return
179
- fi
180
-
181
- local local_aleph="${REPO_DIR}/aleph-client/.venv/bin/aleph"
182
- if [ -x "${local_aleph}" ]; then
183
- printf '%s\n' "${local_aleph}"
184
- return
185
- fi
186
-
187
- if command -v aleph >/dev/null 2>&1; then
188
- command -v aleph
189
- return
190
- fi
191
-
192
- echo "Missing aleph CLI. Set ALEPH_BIN=/path/to/aleph or install aleph-client." >&2
193
- exit 1
194
- }
195
-
196
161
  build_with_host_tools() {
197
162
  echo "Using host virt-customize/qemu-img toolchain."
198
163
  "${SCRIPT_DIR}/build-rootfs-image.sh"
@@ -412,10 +377,7 @@ EOF
412
377
  }
413
378
 
414
379
  upload_image() {
415
- local aleph_bin
416
- aleph_bin="$(resolve_aleph_bin)"
417
-
418
- require python3
380
+ require node
419
381
  require curl
420
382
  [ -f "${IMAGE}" ] || die "Rootfs image does not exist: ${IMAGE}"
421
383
 
@@ -430,48 +392,20 @@ upload_image() {
430
392
  fi
431
393
 
432
394
  local cid
433
- if ! cid="$(python3 - "${OUT_DIR}/ipfs-add-response.jsonl" <<'PY'
434
- import json
435
- import sys
436
- from pathlib import Path
437
-
438
- lines = [line for line in Path(sys.argv[1]).read_text().splitlines() if line.strip()]
439
- if not lines:
440
- raise SystemExit("No response received from the IPFS add endpoint")
441
-
442
- payload = json.loads(lines[-1])
443
- cid = payload.get("Hash")
444
- if not cid:
445
- raise SystemExit(f"IPFS add response did not include a Hash: {payload}")
446
-
447
- print(cid)
448
- PY
449
- )"; then
395
+ if ! cid="$(
396
+ node -e "const fs=require('node:fs');const lines=fs.readFileSync(process.argv[1],'utf8').split(/\\r?\\n/).map((line)=>line.trim()).filter(Boolean);if(!lines.length){console.error('No response received from the IPFS add endpoint');process.exit(1)}const payload=JSON.parse(lines.at(-1));if(typeof payload.Hash!=='string'||!payload.Hash.trim()){console.error('IPFS add response did not include a Hash');process.exit(1)}console.log(payload.Hash.trim());" "${OUT_DIR}/ipfs-add-response.jsonl"
397
+ )"; then
450
398
  die "Failed to extract CID from ${OUT_DIR}/ipfs-add-response.jsonl"
451
399
  fi
452
400
  [ -n "${cid}" ] || die "IPFS upload returned an empty CID"
453
401
 
454
- echo "Pinning CID ${cid} on Aleph Cloud with ${aleph_bin}..." >&2
402
+ echo "Pinning CID ${cid} on Aleph Cloud via direct API call..." >&2
455
403
  : > "${OUT_DIR}/store-message.json"
456
- if ! "${aleph_bin}" file pin "${cid}" \
457
- --channel "${CHANNEL}" \
458
- > "${OUT_DIR}/store-message.json"; then
404
+ if ! node "${SCRIPT_DIR}/publish-store-message.mjs" "${cid}" > "${OUT_DIR}/store-message.json"; then
459
405
  die "Aleph pin failed for CID ${cid}"
460
406
  fi
461
407
 
462
- if ! python3 - "${OUT_DIR}/store-message.json" <<'PY'
463
- import json
464
- import sys
465
- from pathlib import Path
466
-
467
- content = Path(sys.argv[1]).read_text().strip()
468
- if not content:
469
- raise SystemExit("Aleph pin returned an empty response")
470
-
471
- payload = json.loads(content)
472
- print(payload["item_hash"])
473
- PY
474
- then
408
+ if ! node -e "const fs=require('node:fs');const content=fs.readFileSync(process.argv[1],'utf8').trim();if(!content){console.error('Aleph pin returned an empty response');process.exit(1)}const payload=JSON.parse(content);if(typeof payload.item_hash!=='string'||!payload.item_hash.trim()){console.error('Failed to extract item_hash');process.exit(1)}console.log(payload.item_hash.trim());" "${OUT_DIR}/store-message.json"; then
475
409
  die "Failed to extract item_hash from ${OUT_DIR}/store-message.json"
476
410
  fi
477
411
  }
@@ -24,7 +24,7 @@ ENV_FILE = os.environ.get("ENV_FILE", "/etc/default/orbitdb-relay-pinner")
24
24
  DESCRIBE_SCRIPT = os.environ.get(
25
25
  "DESCRIBE_SCRIPT", "/usr/local/sbin/orbitdb-relay-pinner-describe.py"
26
26
  )
27
- DEFAULT_API_HOST = os.environ.get("ALEPH_BOOTSTRAP_API_HOST", "https://api2.aleph.im")
27
+ DEFAULT_API_HOST = os.environ.get("ALEPH_BOOTSTRAP_API_HOST", "https://api.aleph.im")
28
28
  DEFAULT_CHANNEL = os.environ.get("ALEPH_BOOTSTRAP_CHANNEL", "simple-todo")
29
29
  DEFAULT_REF = os.environ.get("ALEPH_BOOTSTRAP_REF", "simple-todo-bootstrap")
30
30
  DEFAULT_POST_TYPE = os.environ.get("ALEPH_BOOTSTRAP_POST_TYPE", "relay-bootstrap")
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { createHash } from 'node:crypto'
4
+
5
+ const [{ Wallet }] = await Promise.all([
6
+ import('ethers'),
7
+ ])
8
+
9
+ function requiredEnv(name) {
10
+ const value = process.env[name]?.trim()
11
+ if (!value) {
12
+ throw new Error(`Missing required environment variable ${name}`)
13
+ }
14
+ return value
15
+ }
16
+
17
+ function normalizeApiHost(value) {
18
+ return value.replace(/\/+$/u, '')
19
+ }
20
+
21
+ const cid = process.argv[2]?.trim()
22
+ if (!cid) {
23
+ throw new Error('Usage: publish-store-message.mjs <cid>')
24
+ }
25
+
26
+ const wallet = new Wallet(requiredEnv('ALEPH_PRIVATE_KEY'))
27
+ const apiHost = normalizeApiHost(process.env.ALEPH_API_HOST?.trim() || 'https://api.aleph.im')
28
+ const channel = process.env.CHANNEL?.trim() || 'ALEPH-CLOUDSOLUTIONS'
29
+ const ref = process.env.ALEPH_ROOTFS_REF?.trim() || undefined
30
+ const time = Date.now() / 1000
31
+
32
+ const content = {
33
+ address: wallet.address,
34
+ time,
35
+ item_type: 'ipfs',
36
+ item_hash: cid,
37
+ ...(ref ? { ref } : {}),
38
+ }
39
+ const itemContent = JSON.stringify(content)
40
+ const itemHash = createHash('sha256').update(itemContent).digest('hex')
41
+ const message = {
42
+ sender: wallet.address,
43
+ chain: 'ETH',
44
+ type: 'STORE',
45
+ item_hash: itemHash,
46
+ item_type: 'inline',
47
+ item_content: itemContent,
48
+ time,
49
+ channel,
50
+ }
51
+ const signaturePayload = [message.chain, message.sender, message.type, message.item_hash].join('\n')
52
+ const signature = await wallet.signMessage(signaturePayload)
53
+
54
+ const response = await fetch(`${apiHost}/api/v0/messages`, {
55
+ method: 'POST',
56
+ headers: {
57
+ 'content-type': 'application/json',
58
+ },
59
+ body: JSON.stringify({
60
+ sync: true,
61
+ message: {
62
+ ...message,
63
+ signature,
64
+ },
65
+ }),
66
+ })
67
+
68
+ const payload = await response.json().catch(() => ({}))
69
+ process.stdout.write(`${JSON.stringify(payload)}\n`)
70
+
71
+ if (!response.ok) {
72
+ process.exit(1)
73
+ }
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readFileSync } from 'node:fs'
4
+ import path from 'node:path'
5
+
6
+ function shellQuote(value) {
7
+ return `'${String(value).replace(/'/g, `'\\''`)}'`
8
+ }
9
+
10
+ function shellAssign(name, value) {
11
+ return `${name}=${shellQuote(value)}`
12
+ }
13
+
14
+ const contractPath = process.argv[2]
15
+ if (!contractPath) {
16
+ console.error('Usage: read-rootfs-contract.mjs /path/to/contract.json')
17
+ process.exit(1)
18
+ }
19
+
20
+ let payload
21
+ try {
22
+ payload = JSON.parse(readFileSync(contractPath, 'utf8'))
23
+ } catch (error) {
24
+ console.error(`Failed to read rootfs contract ${contractPath}: ${error instanceof Error ? error.message : String(error)}`)
25
+ process.exit(1)
26
+ }
27
+
28
+ const rootfs = payload.rootfs ?? {}
29
+ const services = payload.services ?? {}
30
+ const source = payload.source ?? {}
31
+ const manifest = payload.manifest ?? {}
32
+ const ports = Array.isArray(payload.ports) ? payload.ports : null
33
+ const profile = typeof rootfs.profile === 'string' ? rootfs.profile.trim() : ''
34
+ const installMode = typeof rootfs.installMode === 'string' ? rootfs.installMode.trim() : ''
35
+
36
+ if (!profile) {
37
+ console.error('Rootfs contract is missing rootfs.profile')
38
+ process.exit(1)
39
+ }
40
+ if (!installMode) {
41
+ console.error('Rootfs contract is missing rootfs.installMode')
42
+ process.exit(1)
43
+ }
44
+ if (!ports) {
45
+ console.error('Rootfs contract field ports must be a list')
46
+ process.exit(1)
47
+ }
48
+
49
+ const resolvedContractPath = path.resolve(contractPath)
50
+ const lines = [
51
+ shellAssign('ROOTFS_CONTRACT_PATH', resolvedContractPath),
52
+ shellAssign('ROOTFS_CONTRACT_ID', payload.id ?? ''),
53
+ shellAssign('ROOTFS_CONTRACT_PROFILE', profile),
54
+ shellAssign('ROOTFS_CONTRACT_INSTALL_MODE', installMode),
55
+ shellAssign('ROOTFS_CONTRACT_SOURCE_SUBDIRECTORY', source.subdirectory ?? ''),
56
+ shellAssign('ROOTFS_CONTRACT_INSTALL_DIR', rootfs.installDir ?? ''),
57
+ shellAssign('ROOTFS_CONTRACT_BINARY_PATH', rootfs.binaryPath ?? '/usr/local/bin/universal-chat-go'),
58
+ shellAssign('ROOTFS_CONTRACT_DATA_DIR', rootfs.dataDir ?? ''),
59
+ shellAssign('ROOTFS_CONTRACT_ENV_FILE', rootfs.envFile ?? ''),
60
+ shellAssign('ROOTFS_CONTRACT_MAIN_SERVICE', services.main ?? ''),
61
+ shellAssign('ROOTFS_CONTRACT_BOOTSTRAP_SERVICE', services.bootstrap ?? ''),
62
+ shellAssign('ROOTFS_CONTRACT_AUTOTLS_SERVICE', services.autotlsRefresh ?? ''),
63
+ shellAssign('ROOTFS_CONTRACT_MANIFEST_COPY_TARGET', manifest.copyTarget ?? ''),
64
+ shellAssign('ROOTFS_CONTRACT_MANIFEST_NOTES', manifest.notes ?? ''),
65
+ shellAssign('ROOTFS_CONTRACT_PORT_FORWARDS_JSON', JSON.stringify(ports)),
66
+ ]
67
+
68
+ process.stdout.write(`${lines.join('\n')}\n`)
@@ -15,7 +15,7 @@ SKIP_UPLOAD="${SKIP_UPLOAD:-0}"
15
15
  SKIP_BUILD="${SKIP_BUILD:-0}"
16
16
  IPFS_ADD_URL="${IPFS_ADD_URL:-https://ipfs.aleph.cloud/api/v0/add}"
17
17
  IPFS_GATEWAY_URL="${IPFS_GATEWAY_URL:-https://ipfs.aleph.cloud/ipfs}"
18
- ALEPH_API_HOST="${ALEPH_API_HOST:-https://api2.aleph.im}"
18
+ ALEPH_API_HOST="${ALEPH_API_HOST:-https://api.aleph.im}"
19
19
  IPFS_GATEWAY_WAIT_ATTEMPTS="${IPFS_GATEWAY_WAIT_ATTEMPTS:-30}"
20
20
  IPFS_GATEWAY_WAIT_DELAY_SECONDS="${IPFS_GATEWAY_WAIT_DELAY_SECONDS:-10}"
21
21
  ROOTFS_CID=""
@@ -20,7 +20,7 @@ except ImportError as error: # pragma: no cover - runtime dependency
20
20
 
21
21
  ENV_FILE = os.environ.get("ENV_FILE", "/etc/default/uc-go-peer")
22
22
  DESCRIBE_SCRIPT = os.environ.get("DESCRIBE_SCRIPT", "/usr/local/sbin/uc-go-peer-describe.py")
23
- DEFAULT_API_HOST = os.environ.get("ALEPH_BOOTSTRAP_API_HOST", "https://api2.aleph.im")
23
+ DEFAULT_API_HOST = os.environ.get("ALEPH_BOOTSTRAP_API_HOST", "https://api.aleph.im")
24
24
  DEFAULT_CHANNEL = os.environ.get("ALEPH_BOOTSTRAP_CHANNEL", "simple-todo")
25
25
  DEFAULT_REF = os.environ.get("ALEPH_BOOTSTRAP_REF", "simple-todo-bootstrap")
26
26
  DEFAULT_POST_TYPE = os.environ.get("ALEPH_BOOTSTRAP_POST_TYPE", "relay-bootstrap")
@@ -22,7 +22,7 @@ except ImportError as error: # pragma: no cover - runtime dependency
22
22
 
23
23
  ENV_FILE = os.environ.get("ENV_FILE", "/etc/default/uc-go-peer")
24
24
  DESCRIBE_SCRIPT = os.environ.get("DESCRIBE_SCRIPT", "/usr/local/sbin/uc-go-peer-describe.py")
25
- DEFAULT_API_HOST = os.environ.get("ALEPH_BOOTSTRAP_API_HOST", "https://api2.aleph.im")
25
+ DEFAULT_API_HOST = os.environ.get("ALEPH_BOOTSTRAP_API_HOST", "https://api.aleph.im")
26
26
  DEFAULT_CHANNEL = os.environ.get("ALEPH_BOOTSTRAP_CHANNEL", "simple-todo")
27
27
  DEFAULT_REF = os.environ.get("ALEPH_BOOTSTRAP_REF", "simple-todo-bootstrap")
28
28
  DEFAULT_POST_TYPE = os.environ.get("ALEPH_BOOTSTRAP_POST_TYPE", "relay-bootstrap")
@@ -24,7 +24,7 @@ BOOTSTRAP_SERVICE = os.environ.get("BOOTSTRAP_SERVICE", "uc-go-peer-bootstrap.se
24
24
  METADATA_FILE = os.environ.get("METADATA_FILE", "/run/uc-go-peer-setup-metadata.json")
25
25
  METADATA_ERROR_FILE = os.environ.get("METADATA_ERROR_FILE", "/run/uc-go-peer-setup-metadata.error")
26
26
  AUTHORIZED_KEYS_FILE = os.environ.get("AUTHORIZED_KEYS_FILE", "/root/.ssh/authorized_keys")
27
- ALEPH_API_HOST = os.environ.get("ALEPH_API_HOST", "https://api2.aleph.im")
27
+ ALEPH_API_HOST = os.environ.get("ALEPH_API_HOST", "https://api.aleph.im")
28
28
  BOOTSTRAP_CONFIG_KEY = os.environ.get("BOOTSTRAP_CONFIG_KEY", "vm-bootstrap-config")
29
29
  BOOTSTRAP_CONFIG_POLL_SECONDS = float(os.environ.get("BOOTSTRAP_CONFIG_POLL_SECONDS", "5"))
30
30
  BOOTSTRAP_CONFIG_SIGNAL_REF = os.environ.get("BOOTSTRAP_CONFIG_SIGNAL_REF", "vm-bootstrap-config")
@@ -1,72 +0,0 @@
1
- #!/usr/bin/env python3
2
-
3
- from __future__ import annotations
4
-
5
- import argparse
6
- import json
7
- import shlex
8
- import sys
9
- from pathlib import Path
10
-
11
-
12
- def shell_assign(name: str, value: str) -> str:
13
- return f"{name}={shlex.quote(value)}"
14
-
15
-
16
- def main() -> int:
17
- parser = argparse.ArgumentParser(description="Read a relay rootfs contract and emit shell exports.")
18
- parser.add_argument("contract", type=Path, help="Path to the rootfs contract JSON file")
19
- args = parser.parse_args()
20
-
21
- try:
22
- payload = json.loads(args.contract.read_text())
23
- except FileNotFoundError:
24
- print(f"Missing rootfs contract: {args.contract}", file=sys.stderr)
25
- return 1
26
- except json.JSONDecodeError as exc:
27
- print(f"Invalid rootfs contract JSON in {args.contract}: {exc}", file=sys.stderr)
28
- return 1
29
-
30
- rootfs = payload.get("rootfs") or {}
31
- services = payload.get("services") or {}
32
- source = payload.get("source") or {}
33
- manifest = payload.get("manifest") or {}
34
- ports = payload.get("ports") or []
35
-
36
- profile = rootfs.get("profile")
37
- install_mode = rootfs.get("installMode")
38
-
39
- if not isinstance(profile, str) or not profile.strip():
40
- print("Rootfs contract is missing rootfs.profile", file=sys.stderr)
41
- return 1
42
- if not isinstance(install_mode, str) or not install_mode.strip():
43
- print("Rootfs contract is missing rootfs.installMode", file=sys.stderr)
44
- return 1
45
- if not isinstance(ports, list):
46
- print("Rootfs contract field ports must be a list", file=sys.stderr)
47
- return 1
48
-
49
- lines = [
50
- shell_assign("ROOTFS_CONTRACT_PATH", str(args.contract.resolve())),
51
- shell_assign("ROOTFS_CONTRACT_ID", str(payload.get("id", ""))),
52
- shell_assign("ROOTFS_CONTRACT_PROFILE", profile.strip()),
53
- shell_assign("ROOTFS_CONTRACT_INSTALL_MODE", install_mode.strip()),
54
- shell_assign("ROOTFS_CONTRACT_SOURCE_SUBDIRECTORY", str(source.get("subdirectory", ""))),
55
- shell_assign("ROOTFS_CONTRACT_INSTALL_DIR", str(rootfs.get("installDir", ""))),
56
- shell_assign("ROOTFS_CONTRACT_BINARY_PATH", str(rootfs.get("binaryPath", "/usr/local/bin/universal-chat-go"))),
57
- shell_assign("ROOTFS_CONTRACT_DATA_DIR", str(rootfs.get("dataDir", ""))),
58
- shell_assign("ROOTFS_CONTRACT_ENV_FILE", str(rootfs.get("envFile", ""))),
59
- shell_assign("ROOTFS_CONTRACT_MAIN_SERVICE", str(services.get("main", ""))),
60
- shell_assign("ROOTFS_CONTRACT_BOOTSTRAP_SERVICE", str(services.get("bootstrap", ""))),
61
- shell_assign("ROOTFS_CONTRACT_AUTOTLS_SERVICE", str(services.get("autotlsRefresh", ""))),
62
- shell_assign("ROOTFS_CONTRACT_MANIFEST_COPY_TARGET", str(manifest.get("copyTarget", ""))),
63
- shell_assign("ROOTFS_CONTRACT_MANIFEST_NOTES", str(manifest.get("notes", ""))),
64
- shell_assign("ROOTFS_CONTRACT_PORT_FORWARDS_JSON", json.dumps(ports, separators=(",", ":"))),
65
- ]
66
-
67
- print("\n".join(lines))
68
- return 0
69
-
70
-
71
- if __name__ == "__main__":
72
- raise SystemExit(main())