Compare commits

..

110 Commits

Author SHA1 Message Date
e21815b0e2 5.3.1
Some checks failed
Default (tags) / security (push) Successful in 40s
Default (tags) / test (push) Failing after 4m4s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-08-15 18:14:57 +00:00
5f1090dd62 fix(AppData/Conversion): Improve boolean conversion and mapping evaluation in AppData, ensuring falsy values (like false, 0, and empty strings) are correctly handled and logged. Also, reduce test timeout and add local permissions settings for development. 2025-08-15 18:14:57 +00:00
b0df896a14 5.3.0
Some checks failed
Default (tags) / security (push) Successful in 40s
Default (tags) / test (push) Failing after 4m3s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-08-15 13:17:18 +00:00
ed969cee47 feat(AppData): Refactor AppData class for declarative env mapping and enhanced static helpers 2025-08-15 13:17:18 +00:00
61fafd2c8f 5.1.4
Some checks failed
Default (tags) / security (push) Successful in 34s
Default (tags) / test (push) Failing after 4m4s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-08-15 12:26:50 +00:00
33a5b6b11c fix(AppData, dev dependencies, settings): Improve boolean conversion in AppData, update @types/node dependency, and add local settings file. 2025-08-15 12:26:50 +00:00
c74ded8d5d 5.1.3
Some checks failed
Default (tags) / security (push) Successful in 41s
Default (tags) / test (push) Failing after 4m3s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-08-15 12:12:26 +00:00
62e61168a0 fix(appdata): Fix iteration over overwriteObject in AppData and update configuration for dependency and path handling 2025-08-15 12:12:26 +00:00
5e0edecf18 5.1.2
Some checks failed
Default (tags) / security (push) Failing after 15s
Default (tags) / test (push) Failing after 11s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2024-11-06 03:48:39 +01:00
70cefc00fa fix(appdata): Fix iteration over overwriteObject in AppData class 2024-11-06 03:48:39 +01:00
6f14c73b5f 5.1.1 2024-11-05 21:29:26 +01:00
1e6f636608 fix(AppData): Fix issue with overwrite object handling in AppData class 2024-11-05 21:29:26 +01:00
eff77f8976 5.1.0 2024-11-05 15:11:02 +01:00
b5f109d320 feat(appdata): Add support for overwriting keys using the overwriteObject option in AppData 2024-11-05 15:11:01 +01:00
3a53938e8e 5.0.23 2024-06-19 19:03:26 +02:00
db90714a81 fix(core): update 2024-06-19 19:03:26 +02:00
b81ab9d9b2 5.0.22 2024-06-19 18:50:01 +02:00
5ec9124d29 fix(core): update 2024-06-19 18:50:00 +02:00
f7f035e878 5.0.21 2024-06-19 17:53:28 +02:00
3caf300544 fix(core): update 2024-06-19 17:53:27 +02:00
6a70af9b6f 5.0.20 2024-06-19 17:53:11 +02:00
2ad3da85a9 fix(core): update 2024-06-19 17:53:10 +02:00
09e8b8b94c 5.0.19 2024-06-19 17:43:56 +02:00
265f4df8b3 fix(core): update 2024-06-19 17:43:55 +02:00
5461ec0636 5.0.18 2024-06-19 15:07:50 +02:00
937252f99e fix(core): update 2024-06-19 15:07:49 +02:00
6f19c3cc71 5.0.17 2024-06-12 20:21:52 +02:00
4be54f6dcc fix(core): update 2024-06-12 20:21:51 +02:00
5c6702b898 5.0.16 2024-06-12 20:18:28 +02:00
f1ecda411a fix(core): update 2024-06-12 20:18:27 +02:00
267d2c392d 5.0.15 2024-06-12 20:10:20 +02:00
2ca82fb28b fix(core): update 2024-06-12 20:10:19 +02:00
b342602ca0 5.0.14 2024-06-12 20:04:05 +02:00
bb72e1c2c0 fix(core): update 2024-06-12 20:04:04 +02:00
40ad00e75a update description 2024-05-29 14:11:24 +02:00
de1db395cd 5.0.13 2024-04-14 03:45:31 +02:00
83968d578c fix(core): update 2024-04-14 03:45:31 +02:00
155cd8bd6a 5.0.12 2024-04-14 02:10:29 +02:00
5eebd0fd02 fix(core): update 2024-04-14 02:10:29 +02:00
686aa0d2e7 5.0.11 2024-04-14 02:09:41 +02:00
f97d72da28 fix(core): update 2024-04-14 02:09:41 +02:00
78d1540ce0 update npmextra.json: githost 2024-04-01 21:33:26 +02:00
916dd116c1 update npmextra.json: githost 2024-04-01 19:57:30 +02:00
5fd57cb56b update npmextra.json: githost 2024-03-30 21:46:27 +01:00
1a106e322c 5.0.10 2024-02-13 02:10:21 +01:00
36ded40032 fix(core): update 2024-02-13 02:10:21 +01:00
44f5f84195 5.0.9 2024-02-13 02:09:00 +01:00
72655a6a0e fix(core): update 2024-02-13 02:08:59 +01:00
961df11121 5.0.8 2024-02-13 02:04:05 +01:00
ee6f5e63ee fix(core): update 2024-02-13 02:04:04 +01:00
a40b6adeef 5.0.7 2024-02-13 02:01:39 +01:00
6ee324a0ef fix(core): update 2024-02-13 02:01:39 +01:00
215a48a409 5.0.6 2024-02-13 01:54:51 +01:00
aeaa6149e4 fix(core): update 2024-02-13 01:54:50 +01:00
75059dfca8 5.0.5 2024-02-13 01:46:23 +01:00
90c0f30ce1 fix(core): update 2024-02-13 01:46:22 +01:00
9a55303978 5.0.4 2024-02-13 00:50:51 +01:00
224125fb8e fix(core): update 2024-02-13 00:50:50 +01:00
df9d197508 5.0.3 2024-02-13 00:48:45 +01:00
ae33716af4 fix(core): update 2024-02-13 00:48:44 +01:00
31b64eda5e 5.0.2 2024-02-12 20:09:27 +01:00
7f3437e3cd fix(core): update 2024-02-12 20:09:26 +01:00
8903bee78d 5.0.1 2024-02-12 19:16:58 +01:00
a02b45a673 fix(core): update 2024-02-12 19:16:57 +01:00
e7883f5997 5.0.0 2024-02-12 19:16:44 +01:00
39ab01b4af BREAKING CHANGE(core): update 2024-02-12 19:16:43 +01:00
bb9de1b13b 4.0.16 2024-02-12 18:40:01 +01:00
080e133e9f fix(core): update 2024-02-12 18:40:01 +01:00
a284c58a68 4.0.15 2024-02-10 04:55:51 +01:00
18bb54831d fix(core): update 2024-02-10 04:55:50 +01:00
141c7ed8a7 4.0.14 2024-02-10 04:54:01 +01:00
037481f195 fix(core): update 2024-02-10 04:54:00 +01:00
e414e392d3 4.0.13 2024-02-09 15:57:32 +01:00
4f7e382bc9 fix(core): update 2024-02-09 15:57:32 +01:00
1c0b428606 4.0.12 2024-02-09 13:47:52 +01:00
90e8625771 fix(core): update 2024-02-09 13:47:51 +01:00
abbce0d4a1 4.0.11 2024-02-09 11:52:31 +01:00
93c65acc95 fix(core): update 2024-02-09 11:52:30 +01:00
a68d59a4d2 4.0.10 2024-02-09 05:16:02 +01:00
0133dca698 fix(core): update 2024-02-09 05:16:02 +01:00
6174490e8e 4.0.9 2024-02-08 14:09:20 +01:00
d952a761b2 fix(core): update 2024-02-08 14:09:20 +01:00
05909f776e 4.0.8 2024-02-07 21:44:00 +01:00
779883fbab fix(core): update 2024-02-07 21:44:00 +01:00
349f074bb9 4.0.7 2024-02-07 18:16:41 +01:00
d209661586 fix(core): update 2024-02-07 18:16:40 +01:00
3ce6036478 4.0.6 2024-01-27 19:03:06 +01:00
54bf310403 fix(core): update 2024-01-27 19:03:06 +01:00
2ec485048b 4.0.5 2024-01-25 13:57:55 +01:00
caca370e77 fix(core): update 2024-01-25 13:57:55 +01:00
45deb1a8d2 4.0.4 2023-08-26 10:56:21 +02:00
4c9d3c7148 fix(core): update 2023-08-26 10:56:20 +02:00
320c627d4f 4.0.3 2023-08-26 09:56:21 +02:00
d834e0a220 fix(core): update 2023-08-26 09:56:20 +02:00
86427ac05d 4.0.2 2023-08-24 22:59:44 +02:00
dc59682c15 fix(core): update 2023-08-24 22:59:43 +02:00
45ebf0944c 4.0.1 2023-08-24 12:06:47 +02:00
677aa4f0ea fix(core): update 2023-08-24 12:06:46 +02:00
7f0985f24d 4.0.0 2023-08-24 12:02:25 +02:00
5f2f7e2b39 BREAKING CHANGE(core): update 2023-08-24 12:02:24 +02:00
26a6ac9651 3.0.13 2023-08-24 10:44:43 +02:00
072ee31c3f fix(core): update 2023-08-24 10:44:42 +02:00
8fa3fd8ac1 3.0.12 2023-08-24 10:39:47 +02:00
102422c9c7 fix(core): update 2023-08-24 10:39:47 +02:00
2e95824ff4 3.0.11 2023-08-03 19:28:11 +02:00
c85c0d3035 fix(core): update 2023-08-03 19:28:10 +02:00
68da8c7841 3.0.10 2023-08-03 19:22:35 +02:00
a12418ba9c fix(core): update 2023-08-03 19:22:34 +02:00
1664eee3fe switch to new org scheme 2023-07-11 00:04:21 +02:00
6112f430c9 switch to new org scheme 2023-07-10 02:41:42 +02:00
35 changed files with 11425 additions and 11435 deletions

View File

@@ -0,0 +1,66 @@
name: Default (not tags)
on:
push:
tags-ignore:
- '**'
env:
IMAGE: code.foss.global/host.today/ht-docker-node:npmci
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@/${{gitea.repository}}.git
NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}}
NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}}
NPMCI_GIT_GITHUBTOKEN: ${{secrets.NPMCI_GIT_GITHUBTOKEN}}
NPMCI_URL_CLOUDLY: ${{secrets.NPMCI_URL_CLOUDLY}}
jobs:
security:
runs-on: ubuntu-latest
continue-on-error: true
container:
image: ${{ env.IMAGE }}
steps:
- uses: actions/checkout@v3
- name: Install pnpm and npmci
run: |
pnpm install -g pnpm
pnpm install -g @ship.zone/npmci
- name: Run npm prepare
run: npmci npm prepare
- name: Audit production dependencies
run: |
npmci command npm config set registry https://registry.npmjs.org
npmci command pnpm audit --audit-level=high --prod
continue-on-error: true
- name: Audit development dependencies
run: |
npmci command npm config set registry https://registry.npmjs.org
npmci command pnpm audit --audit-level=high --dev
continue-on-error: true
test:
if: ${{ always() }}
needs: security
runs-on: ubuntu-latest
container:
image: ${{ env.IMAGE }}
steps:
- uses: actions/checkout@v3
- name: Test stable
run: |
npmci node install stable
npmci npm install
npmci npm test
- name: Test build
run: |
npmci node install stable
npmci npm install
npmci npm build

View File

@@ -0,0 +1,124 @@
name: Default (tags)
on:
push:
tags:
- '*'
env:
IMAGE: code.foss.global/host.today/ht-docker-node:npmci
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@/${{gitea.repository}}.git
NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}}
NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}}
NPMCI_GIT_GITHUBTOKEN: ${{secrets.NPMCI_GIT_GITHUBTOKEN}}
NPMCI_URL_CLOUDLY: ${{secrets.NPMCI_URL_CLOUDLY}}
jobs:
security:
runs-on: ubuntu-latest
continue-on-error: true
container:
image: ${{ env.IMAGE }}
steps:
- uses: actions/checkout@v3
- name: Prepare
run: |
pnpm install -g pnpm
pnpm install -g @ship.zone/npmci
npmci npm prepare
- name: Audit production dependencies
run: |
npmci command npm config set registry https://registry.npmjs.org
npmci command pnpm audit --audit-level=high --prod
continue-on-error: true
- name: Audit development dependencies
run: |
npmci command npm config set registry https://registry.npmjs.org
npmci command pnpm audit --audit-level=high --dev
continue-on-error: true
test:
if: ${{ always() }}
needs: security
runs-on: ubuntu-latest
container:
image: ${{ env.IMAGE }}
steps:
- uses: actions/checkout@v3
- name: Prepare
run: |
pnpm install -g pnpm
pnpm install -g @ship.zone/npmci
npmci npm prepare
- name: Test stable
run: |
npmci node install stable
npmci npm install
npmci npm test
- name: Test build
run: |
npmci node install stable
npmci npm install
npmci npm build
release:
needs: test
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
container:
image: ${{ env.IMAGE }}
steps:
- uses: actions/checkout@v3
- name: Prepare
run: |
pnpm install -g pnpm
pnpm install -g @ship.zone/npmci
npmci npm prepare
- name: Release
run: |
npmci node install stable
npmci npm publish
metadata:
needs: test
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
container:
image: ${{ env.IMAGE }}
continue-on-error: true
steps:
- uses: actions/checkout@v3
- name: Prepare
run: |
pnpm install -g pnpm
pnpm install -g @ship.zone/npmci
npmci npm prepare
- name: Code quality
run: |
npmci command npm install -g typescript
npmci npm install
- name: Trigger
run: npmci trigger
- name: Build docs and upload artifacts
run: |
npmci node install stable
npmci npm install
pnpm install -g @git.zone/tsdoc
npmci command tsdoc
continue-on-error: true

3
.gitignore vendored
View File

@@ -3,7 +3,6 @@
# artifacts
coverage/
public/
pages/
# installs
node_modules/
@@ -17,4 +16,4 @@ node_modules/
dist/
dist_*/
# custom
#------# custom

View File

@@ -1,137 +0,0 @@
# gitzone ci_default
image: registry.gitlab.com/hosttoday/ht-docker-node:npmci
cache:
paths:
- .npmci_cache/
key: '$CI_BUILD_STAGE'
stages:
- security
- test
- release
- metadata
# ====================
# security stage
# ====================
mirror:
stage: security
script:
- npmci git mirror
only:
- tags
tags:
- lossless
- docker
- notpriv
auditProductionDependencies:
image: registry.gitlab.com/hosttoday/ht-docker-node:npmci
stage: security
script:
- npmci npm prepare
- npmci command npm install --production --ignore-scripts
- npmci command npm config set registry https://registry.npmjs.org
- npmci command npm audit --audit-level=high --only=prod --production
tags:
- docker
auditDevDependencies:
image: registry.gitlab.com/hosttoday/ht-docker-node:npmci
stage: security
script:
- npmci npm prepare
- npmci command npm install --ignore-scripts
- npmci command npm config set registry https://registry.npmjs.org
- npmci command npm audit --audit-level=high --only=dev
tags:
- docker
allow_failure: true
# ====================
# test stage
# ====================
testStable:
stage: test
script:
- npmci npm prepare
- npmci node install stable
- npmci npm install
- npmci npm test
coverage: /\d+.?\d+?\%\s*coverage/
tags:
- docker
testBuild:
stage: test
script:
- npmci npm prepare
- npmci node install stable
- npmci npm install
- npmci command npm run build
coverage: /\d+.?\d+?\%\s*coverage/
tags:
- docker
release:
stage: release
script:
- npmci node install stable
- npmci npm publish
only:
- tags
tags:
- lossless
- docker
- notpriv
# ====================
# metadata stage
# ====================
codequality:
stage: metadata
allow_failure: true
only:
- tags
script:
- npmci command npm install -g tslint typescript
- npmci npm prepare
- npmci npm install
- npmci command "tslint -c tslint.json ./ts/**/*.ts"
tags:
- lossless
- docker
- priv
trigger:
stage: metadata
script:
- npmci trigger
only:
- tags
tags:
- lossless
- docker
- notpriv
pages:
stage: metadata
script:
- npmci node install lts
- npmci command npm install -g @gitzone/tsdoc
- npmci npm prepare
- npmci npm install
- npmci command tsdoc
tags:
- lossless
- docker
- notpriv
only:
- tags
artifacts:
expire_in: 1 week
paths:
- public
allow_failure: true

Binary file not shown.

View File

@@ -0,0 +1,32 @@
# Coding Standards for npmextra
## Naming Conventions
- **Interfaces**: Prefix with `I` (e.g., `IAppDataOptions`, `ITestOptions`)
- **Types**: Prefix with `T` (e.g., `TKeyValueStore`)
- **Filenames**: Always lowercase (e.g., `npmextra.classes.appdata.ts`)
- **Module structure**: `npmextra.<type>.<name>.ts` pattern
## Import/Export Patterns
- Use ES module syntax (`import`/`export`)
- Import all dependencies through `npmextra.plugins.ts`
- Reference with full path: `plugins.moduleName.className()`
- Export all public APIs through `index.ts`
## TypeScript Patterns
- Use generic types for flexibility (`<T = any>`)
- Leverage TypeScript utility types from `@tsclass/tsclass`
- Use async/await for asynchronous operations
- Use Promises with smartpromise utilities
## Testing Standards
- Import expect from `@git.zone/tstest/tapbundle`
- Test files end with `export default tap.start()`
- Use descriptive test names with `tap.test()`
- Test file naming: `test.*.ts` pattern
## Code Quality
- Make focused, goal-oriented changes
- Preserve necessary complexity
- Remove redundancy carefully
- Keep async patterns where they add value
- No comments unless explicitly requested

View File

@@ -0,0 +1,30 @@
# npmextra Project Overview
## Purpose
npmextra is a utility library that enhances npm with additional configuration and tool management capabilities. It provides a key-value store for project setups and centralized configuration management through npmextra.json files.
## Tech Stack
- TypeScript (ES modules)
- Node.js
- Dependencies:
- @push.rocks/qenv - Environment variable management
- @push.rocks/smartfile - File system operations
- @push.rocks/smartjson - JSON handling
- @push.rocks/smartlog - Logging
- @push.rocks/smartpath - Path utilities
- @push.rocks/smartpromise - Promise utilities
- @push.rocks/smartrx - Reactive programming
- @push.rocks/taskbuffer - Task management
- @tsclass/tsclass - TypeScript utilities
## Main Components
1. **Npmextra** - Main class for managing npmextra.json configurations
2. **KeyValueStore** - Persistent key-value storage system
3. **AppData** - Advanced data management with environment variable mapping
## Project Structure
- `ts/` - TypeScript source files
- `test/` - Test files using @git.zone/tstest
- `dist_ts/` - Compiled JavaScript output
- `npmextra.json` - Project configuration
- `package.json` - Node.js package configuration

View File

@@ -0,0 +1,31 @@
# Suggested Commands for npmextra Development
## Build Commands
- `pnpm run build` - Build the TypeScript project (uses tsbuild)
- `pnpm test` - Run tests using tstest
## Development Commands
- `pnpm install` - Install dependencies
- `pnpm install --save-dev <package>` - Add development dependencies
- `tsx <script>` - Run TypeScript files directly (tsx is globally available)
## Git Commands
- `git status` - Check current changes
- `git add .` - Stage changes
- `git commit -m "message"` - Commit changes (only when explicitly requested)
- `git mv` - Move/rename files to preserve history
## Testing
- `pnpm test` - Run all tests
- `tstest test/test.some.ts --verbose` - Run specific test file
- Tests use @git.zone/tstest framework with tap-based structure
## Type Checking
- `pnpm run build` - Type check and build the project
- `tsbuild check test/**/* --skiplibcheck` - Type check test files only
## Directory Structure
- Source code in `ts/` directory
- Tests in `test/` directory
- Built output in `dist_ts/` directory
- Temporary/debug files in `.nogit/` directory

68
.serena/project.yml Normal file
View File

@@ -0,0 +1,68 @@
# language of the project (csharp, python, rust, java, typescript, go, cpp, or ruby)
# * For C, use cpp
# * For JavaScript, use typescript
# Special requirements:
# * csharp: Requires the presence of a .sln file in the project folder.
language: typescript
# whether to use the project's gitignore file to ignore files
# Added on 2025-04-07
ignore_all_files_in_gitignore: true
# list of additional paths to ignore
# same syntax as gitignore, so you can use * and **
# Was previously called `ignored_dirs`, please update your config if you are using that.
# Added (renamed) on 2025-04-07
ignored_paths: []
# whether the project is in read-only mode
# If set to true, all editing tools will be disabled and attempts to use them will result in an error
# Added on 2025-04-18
read_only: false
# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details.
# Below is the complete list of tools for convenience.
# To make sure you have the latest list of tools, and to view their descriptions,
# execute `uv run scripts/print_tool_overview.py`.
#
# * `activate_project`: Activates a project by name.
# * `check_onboarding_performed`: Checks whether project onboarding was already performed.
# * `create_text_file`: Creates/overwrites a file in the project directory.
# * `delete_lines`: Deletes a range of lines within a file.
# * `delete_memory`: Deletes a memory from Serena's project-specific memory store.
# * `execute_shell_command`: Executes a shell command.
# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced.
# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type).
# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type).
# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file.
# * `initial_instructions`: Gets the initial instructions for the current project.
# Should only be used in settings where the system prompt cannot be set,
# e.g. in clients you have no control over, like Claude Desktop.
# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol.
# * `insert_at_line`: Inserts content at a given line in a file.
# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol.
# * `list_dir`: Lists files and directories in the given directory (optionally with recursion).
# * `list_memories`: Lists memories in Serena's project-specific memory store.
# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context).
# * `read_file`: Reads a file within the project directory.
# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store.
# * `remove_project`: Removes a project from the Serena configuration.
# * `replace_lines`: Replaces a range of lines within a file with new content.
# * `replace_symbol_body`: Replaces the full definition of a symbol.
# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen.
# * `search_for_pattern`: Performs a search for a pattern in the project.
# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase.
# * `switch_modes`: Activates modes by providing a list of their names
# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information.
# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task.
# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed.
# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store.
excluded_tools: []
# initial prompt for the project. It will always be given to the LLM upon activating the project
# (contrary to the memories, which are loaded on demand).
initial_prompt: ""
project_name: "npmextra"

4
.snyk
View File

@@ -1,4 +0,0 @@
# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities.
version: v1.12.0
ignore: {}
patch: {}

24
.vscode/launch.json vendored
View File

@@ -2,28 +2,10 @@
"version": "0.2.0",
"configurations": [
{
"name": "current file",
"type": "node",
"command": "npm test",
"name": "Run npm test",
"request": "launch",
"args": [
"${relativeFile}"
],
"runtimeArgs": ["-r", "@gitzone/tsrun"],
"cwd": "${workspaceRoot}",
"protocol": "inspector",
"internalConsoleOptions": "openOnSessionStart"
},
{
"name": "test.ts",
"type": "node",
"request": "launch",
"args": [
"test/test.ts"
],
"runtimeArgs": ["-r", "@gitzone/tsrun"],
"cwd": "${workspaceRoot}",
"protocol": "inspector",
"internalConsoleOptions": "openOnSessionStart"
"type": "node-terminal"
}
]
}

132
changelog.md Normal file
View File

@@ -0,0 +1,132 @@
# Changelog
## 2025-08-15 - 5.3.1 - fix(AppData/Conversion)
Improve boolean conversion and mapping evaluation in AppData, ensuring falsy values (like false, 0, and empty strings) are correctly handled and logged. Also, reduce test timeout and add local permissions settings for development.
- Enhanced toBoolean and evaluateMappingValue functions to properly preserve and log falsy values.
- Added detailed logging for mapping spec processing and nested key evaluations.
- Reduced test timeout in package.json for faster CI feedback.
- Introduced .claude/settings.local.json with updated permissions for local development.
## 2025-08-15 - 5.3.0 - feat(AppData)
Refactor AppData class for declarative env mapping and enhanced static helpers
- Introduced a singleton Qenv provider to optimize environment variable resolution.
- Centralized type conversion logic with utility functions for boolean, JSON, base64, number, and string conversions.
- Replaced complex switch statements with a composable, declarative mapping pipeline for processing envMapping.
- Enhanced logging during AppData initialization to clearly report key processing and overwrite operations.
- Added new static helper methods for environment variable access (valueAsBoolean, valueAsJson, valueAsBase64, valueAsString, valueAsNumber).
- Fixed boolean conversion issues and ensured backward compatibility with the deprecated 'ephermal' option.
## 2025-08-15 - 5.2.0 - feat(AppData)
Major refactoring of AppData class for improved elegance and maintainability
- **New Features:**
- Added static helper methods for direct environment variable access:
- `AppData.valueAsBoolean()` - Convert env vars to boolean
- `AppData.valueAsJson()` - Parse env vars as JSON
- `AppData.valueAsBase64()` - Decode base64 env vars
- `AppData.valueAsString()` - Get env vars as string
- `AppData.valueAsNumber()` - Parse env vars as number
- Enhanced logging for AppData initialization and key processing:
- Shows which storage type is being used (custom, ephemeral, auto-selected)
- Logs each key being processed with its spec type
- Reports success/failure for each key with type information
- Provides summary statistics of processed keys
- **Architecture Improvements:**
- Replaced 100+ line switch statement with declarative pipeline architecture
- Introduced centralized type converters and transform registry
- Implemented composable transform pipeline: `parseMappingSpec()``resolveSource()``applyTransforms()`
- Added singleton Qenv provider to reduce allocations
- Reduced code complexity by ~70% while maintaining 100% backward compatibility
- **Bug Fixes:**
- Fixed boolean conversion to properly handle both string and boolean inputs
- Added `ephemeral` option (correctly spelled) while maintaining backward compatibility with deprecated `ephermal`
- **Performance:**
- Optimized environment variable resolution with shared Qenv instance
- Reduced object allocations in static helpers
## 2025-08-15 - 5.1.4 - fix(AppData, dev dependencies, settings)
Improve boolean conversion in AppData, update @types/node dependency, and add local settings file.
- Fixed env var boolean conversion to properly handle non-string values in AppData.
- Updated @types/node from ^20.14.5 to ^22 in package.json.
- Added .claude/settings.local.json to configure project permissions locally.
## 2025-08-15 - 5.1.3 - fix(appdata)
Fix iteration over overwriteObject in AppData and update configuration for dependency and path handling
- Replaced incorrect looping constructs in the AppData class to properly iterate over overwriteObject keys
- Improved environment variable mapping and file path resolution in multiple TS modules
- Updated dependency versions and adjusted git workflow configurations
- Enhanced project configuration including TS config and build script adjustments
## 2024-11-06 - 5.1.2 - fix(appdata)
Fix iteration over overwriteObject in AppData class
- Corrected the for loop from in to of inside the AppData class for iterating over overwriteObject keys.
## 2024-11-05 - 5.1.1 - fix(AppData)
Fix issue with overwrite object handling in AppData class
- Corrected logic to handle cases when overwriteObject is undefined.
## 2024-11-05 - 5.1.0 - feat(appdata)
Add support for overwriting keys using the overwriteObject option in AppData
- Introduced the overwriteObject option in IAppDataOptions to allow overwriting specific keys in the AppData class.
## 2024-06-19 - 5.0.17 - 5.0.23 - Core Updates
Routine maintenance and updates to the core components.
- Multiple core updates and fixes improving stability
## 2024-06-12 - 5.0.13 - 5.0.16 - Core Updates
Maintenance focus on core systems with enhancements and problem resolutions.
- Enhancements and updates in the core functionality
## 2024-05-29 - 5.0.13 - Documentation Update
Descriptive improvements aligned with current features.
- Updated core description for better clarity in documentation
## 2024-04-01 - 5.0.10 - Configuration Update
Improved configuration management for build processes.
- Updated `npmextra.json` to reflect changes in git repository management
## 2024-02-12 - 5.0.0 - 5.0.9 - Major Core Enhancements
A series of critical updates with resolved issues in the core components.
- Introduction of new core features
- Several core system updates
## 2024-02-12 - 4.0.16 - Major Version Transition
Migration to the new major version with impactful changes.
- BREAKING CHANGE: Significant updates requiring attention for smooth transition
## 2023-08-24 - 3.0.9 - 4.0.16 - Organization Updates
Formatted updates with attention to organizational standards and practice.
- SWITCH to a new organizational scheme
## 2023-07-11 - 3.0.9 - Organizational Enhancement
Shifts aligning with contemporary structuring and logistics.
- Strategic realignment with organizational principles

View File

@@ -11,12 +11,32 @@
"gitzone": {
"projectType": "npm",
"module": {
"githost": "gitlab.com",
"gitscope": "pushrocks",
"githost": "code.foss.global",
"gitscope": "push.rocks",
"gitrepo": "npmextra",
"shortDescription": "do more with npm",
"npmPackagename": "@pushrocks/npmextra",
"license": "MIT"
"description": "A utility to enhance npm with additional configuration, tool management capabilities, and a key-value store for project setups.",
"npmPackagename": "@push.rocks/npmextra",
"license": "MIT",
"keywords": [
"npm",
"configuration management",
"tool management",
"key-value store",
"project setup",
"typescript",
"environment setup",
"dependencies management",
"npm package enhancement",
"automation",
"async operations",
"app configuration",
"smart file handling",
"workflow improvement",
"persistent storage"
]
}
},
"tsdoc": {
"legal": "\n## License and Legal Information\n\nThis repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository. \n\n**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.\n\n### Trademarks\n\nThis project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.\n\n### Company Information\n\nTask Venture Capital GmbH \nRegistered at District court Bremen HRB 35230 HB, Germany\n\nFor any legal inquiries or if you require further information, please contact us via email at hello@task.vc.\n\nBy using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.\n"
}
}
}

11075
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,39 +1,41 @@
{
"name": "@pushrocks/npmextra",
"version": "3.0.9",
"name": "@push.rocks/npmextra",
"version": "5.3.1",
"private": false,
"description": "do more with npm",
"description": "A utility to enhance npm with additional configuration, tool management capabilities, and a key-value store for project setups.",
"main": "dist_ts/index.js",
"typings": "dist_ts/index.d.ts",
"scripts": {
"test": "(tstest test/ --web)",
"build": "(tsbuild --web)"
"test": "(tstest test/ --verbose --logfile --timeout 20)",
"build": "(tsbuild --web --allowimplicitany)",
"buildDocs": "tsdoc"
},
"repository": {
"type": "git",
"url": "git+ssh://git@gitlab.com/pushrocks/npmextra.git"
"url": "https://code.foss.global/push.rocks/npmextra.git"
},
"author": "Lossless GmbH",
"license": "MIT",
"bugs": {
"url": "https://gitlab.com/pushrocks/npmextra/issues"
"url": "https://code.foss.global/push.rocks/npmextra/issues"
},
"homepage": "https://gitlab.com/pushrocks/npmextra#README",
"homepage": "https://code.foss.global/push.rocks/npmextra#readme",
"dependencies": {
"@pushrocks/smartfile": "^8.0.8",
"@pushrocks/smartlog": "^2.0.39",
"@pushrocks/smartpath": "^4.0.3",
"@pushrocks/smartpromise": "^3.1.3",
"@pushrocks/taskbuffer": "^2.1.13"
"@push.rocks/qenv": "^6.1.2",
"@push.rocks/smartfile": "^11.2.5",
"@push.rocks/smartjson": "^5.0.20",
"@push.rocks/smartlog": "^3.1.8",
"@push.rocks/smartpath": "^6.0.0",
"@push.rocks/smartpromise": "^4.2.3",
"@push.rocks/smartrx": "^3.0.10",
"@push.rocks/taskbuffer": "^3.1.7",
"@tsclass/tsclass": "^9.2.0"
},
"devDependencies": {
"@gitzone/tsbuild": "^2.1.25",
"@gitzone/tsrun": "^1.2.12",
"@gitzone/tstest": "^1.0.52",
"@pushrocks/tapbundle": "^3.2.10",
"@types/node": "^14.14.22",
"tslint": "^6.1.3",
"tslint-config-prettier": "^1.18.0"
"@git.zone/tsbuild": "^2.6.4",
"@git.zone/tsrun": "^1.3.3",
"@git.zone/tstest": "^2.3.2",
"@types/node": "^22"
},
"files": [
"ts/**/*",
@@ -49,5 +51,27 @@
],
"browserslist": [
"last 1 chrome versions"
]
],
"type": "module",
"keywords": [
"npm",
"configuration management",
"tool management",
"key-value store",
"project setup",
"typescript",
"environment setup",
"dependencies management",
"npm package enhancement",
"automation",
"async operations",
"app configuration",
"smart file handling",
"workflow improvement",
"persistent storage"
],
"packageManager": "pnpm@10.11.0+sha512.6540583f41cc5f628eb3d9773ecee802f4f9ef9923cc45b69890fb47991d4b092964694ec3a4f738a420c918a333062c8b925d312f42e4f0c263eb603551f977",
"pnpm": {
"overrides": {}
}
}

9218
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

4
pnpm-workspace.yaml Normal file
View File

@@ -0,0 +1,4 @@
onlyBuiltDependencies:
- esbuild
- mongodb-memory-server
- puppeteer

0
readme.hints.md Normal file
View File

536
readme.md
View File

@@ -1,73 +1,517 @@
# @pushrocks/npmextra
do more with npm
# @push.rocks/npmextra 🚀
## Availabililty and Links
* [npmjs.org (npm package)](https://www.npmjs.com/package/@pushrocks/npmextra)
* [gitlab.com (source)](https://gitlab.com/pushrocks/npmextra)
* [github.com (source mirror)](https://github.com/pushrocks/npmextra)
* [docs (typedoc)](https://pushrocks.gitlab.io/npmextra/)
**Supercharge your npm projects with powerful configuration management, tool orchestration, and persistent key-value storage.**
## Status for master
## Install 📦
Status Category | Status Badge
-- | --
GitLab Pipelines | [![pipeline status](https://gitlab.com/pushrocks/npmextra/badges/master/pipeline.svg)](https://lossless.cloud)
GitLab Pipline Test Coverage | [![coverage report](https://gitlab.com/pushrocks/npmextra/badges/master/coverage.svg)](https://lossless.cloud)
npm | [![npm downloads per month](https://badgen.net/npm/dy/@pushrocks/npmextra)](https://lossless.cloud)
Snyk | [![Known Vulnerabilities](https://badgen.net/snyk/pushrocks/npmextra)](https://lossless.cloud)
TypeScript Support | [![TypeScript](https://badgen.net/badge/TypeScript/>=%203.x/blue?icon=typescript)](https://lossless.cloud)
node Support | [![node](https://img.shields.io/badge/node->=%2010.x.x-blue.svg)](https://nodejs.org/dist/latest-v10.x/docs/api/)
Code Style | [![Code Style](https://badgen.net/badge/style/prettier/purple)](https://lossless.cloud)
PackagePhobia (total standalone install weight) | [![PackagePhobia](https://badgen.net/packagephobia/install/@pushrocks/npmextra)](https://lossless.cloud)
PackagePhobia (package size on registry) | [![PackagePhobia](https://badgen.net/packagephobia/publish/@pushrocks/npmextra)](https://lossless.cloud)
BundlePhobia (total size when bundled) | [![BundlePhobia](https://badgen.net/bundlephobia/minzip/@pushrocks/npmextra)](https://lossless.cloud)
Platform support | [![Supports Windows 10](https://badgen.net/badge/supports%20Windows%2010/yes/green?icon=windows)](https://lossless.cloud) [![Supports Mac OS X](https://badgen.net/badge/supports%20Mac%20OS%20X/yes/green?icon=apple)](https://lossless.cloud)
```bash
# Using npm
npm install @push.rocks/npmextra --save
## Usage
# Using pnpm (recommended)
pnpm add @push.rocks/npmextra
```
Use TypeScript for best in class instellisense.
## Overview 🎯
## Using npmextra for managing toolconfigs
`@push.rocks/npmextra` is your Swiss Army knife for npm project configuration. It eliminates configuration sprawl by centralizing tool settings, providing intelligent key-value storage, and offering powerful environment variable mapping with automatic type conversions.
The main purpose of npmextra is to configure different tools using one npmextra.json. This way complex setups avoid additional complexity caused by too many configuration files.
### Why npmextra?
npmextra.json
- **🎛️ Centralized Configuration**: Manage all your tool configs in one `npmextra.json` file
- **💾 Persistent Storage**: Smart key-value store with multiple storage strategies
- **🔐 Environment Mapping**: Sophisticated env var handling with automatic type conversion
- **🏗️ TypeScript First**: Full type safety and IntelliSense support
- **⚡ Zero Config**: Works out of the box with sensible defaults
- **🔄 Reactive**: Built-in change detection and observables
## Core Concepts 🏗️
### 1. Npmextra Configuration Management
Stop scattering configuration across dozens of files. Centralize everything in `npmextra.json`:
```typescript
import { Npmextra } from '@push.rocks/npmextra';
// Initialize with current directory
const npmextra = new Npmextra();
// Or specify a custom path
const npmextra = new Npmextra('/path/to/project');
// Get merged configuration for any tool
const eslintConfig = npmextra.dataFor<EslintConfig>('eslint', {
// Default values if not in npmextra.json
extends: 'standard',
rules: {}
});
```
**npmextra.json example:**
```json
{
"sometool": {
"defaultKey1": "awesomeValueFromConfig"
"eslint": {
"extends": "@company/eslint-config",
"rules": {
"no-console": "warn"
}
},
"prettier": {
"semi": false,
"singleQuote": true
}
}
```
```typescript
import { Npmextra } from 'npmextra';
### 2. KeyValueStore - Persistent Data Storage
let myNpmExtra = new Npmextra('my/path/to/cwd'); // cwd argument is optional
mergedData = myNpmExtra.dataFor('sometool', {
// gets merged with whatever is in the configfile
defaultKey1: 'defaultValue1', // so this will get overwritten with "awesomeValueFromConfig"
defaultKey2: 'defaultValue2', // this one will pass through unaltered
A flexible key-value store that persists data between script executions:
```typescript
import { KeyValueStore } from '@push.rocks/npmextra';
interface UserSettings {
username: string;
apiKey: string;
preferences: {
theme: 'light' | 'dark';
notifications: boolean;
};
}
// Different storage strategies
const kvStore = new KeyValueStore<UserSettings>({
typeArg: 'userHomeDir', // Store in user's home directory
identityArg: 'myApp',
mandatoryKeys: ['username', 'apiKey']
});
// Write operations
await kvStore.writeKey('username', 'john_doe');
await kvStore.writeKey('preferences', {
theme: 'dark',
notifications: true
});
// Read operations
const username = await kvStore.readKey('username');
const allData = await kvStore.readAll();
// Check for missing mandatory keys
const missingKeys = await kvStore.getMissingMandatoryKeys();
if (missingKeys.length > 0) {
console.log('Missing required configuration:', missingKeys);
}
// Wait for keys to be present
await kvStore.waitForKeysPresent(['apiKey']);
```
**Storage Types:**
- `userHomeDir`: Store in user's home directory
- `custom`: Specify your own path
- `ephemeral`: In-memory only (perfect for testing)
### 3. AppData - Advanced Environment Management 🌟
The crown jewel of npmextra - sophisticated environment variable mapping with automatic type conversion:
```typescript
import { AppData } from '@push.rocks/npmextra';
interface AppConfig {
apiUrl: string;
apiKey: string;
port: number;
features: {
analytics: boolean;
payment: boolean;
};
cache: {
ttl: number;
redis: {
host: string;
password: string;
};
};
}
const appData = await AppData.createAndInit<AppConfig>({
dirPath: '/app/config', // Optional: defaults to smart path selection
requiredKeys: ['apiKey', 'apiUrl'],
envMapping: {
apiUrl: 'API_URL', // Simple mapping
apiKey: 'hard:development-key-123', // Hardcoded value
port: 'hard:3000', // Hardcoded number
features: {
analytics: 'boolean:ENABLE_ANALYTICS', // Force boolean conversion
payment: 'hard_boolean:true' // Hardcoded boolean
},
cache: {
ttl: 'json:CACHE_CONFIG', // Parse JSON from env var
redis: {
host: 'REDIS_HOST',
password: 'base64:REDIS_PASSWORD_B64' // Decode base64
}
}
},
overwriteObject: {
// Force these values regardless of env vars
port: 8080
}
});
const store = await appData.getKvStore();
const apiUrl = await store.readKey('apiUrl');
```
## AppData Special Cases & Conversions 🎯
### Environment Variable Prefixes
AppData supports sophisticated type conversion through prefixes:
| Prefix | Description | Example | Result |
|--------|-------------|---------|--------|
| `hard:` | Hardcoded value | `hard:myvalue` | `"myvalue"` |
| `hard_boolean:` | Hardcoded boolean | `hard_boolean:true` | `true` |
| `hard_json:` | Hardcoded JSON | `hard_json:{"key":"value"}` | `{key: "value"}` |
| `hard_base64:` | Hardcoded base64 | `hard_base64:SGVsbG8=` | `"Hello"` |
| `boolean:` | Env var as boolean | `boolean:FEATURE_FLAG` | `true/false` |
| `json:` | Parse env var as JSON | `json:CONFIG_JSON` | Parsed object |
| `base64:` | Decode env var from base64 | `base64:SECRET_B64` | Decoded string |
### Automatic Suffix Detection
Variables ending with certain suffixes get automatic conversion:
```typescript
{
envMapping: {
// Automatically parsed as JSON if MY_CONFIG_JSON="{"enabled":true}"
config: 'MY_CONFIG_JSON',
// Automatically decoded from base64 if SECRET_KEY_BASE64="SGVsbG8="
secret: 'SECRET_KEY_BASE64'
}
}
```
### Complex Examples
```typescript
const appData = await AppData.createAndInit({
envMapping: {
// Simple environment variable
apiUrl: 'API_URL',
// Hardcoded values with type conversion
debugMode: 'hard_boolean:false',
maxRetries: 'hard:5',
defaultConfig: 'hard_json:{"timeout":30,"retries":3}',
// Environment variables with conversion
features: 'json:FEATURE_FLAGS', // Expects: {"feature1":true,"feature2":false}
isProduction: 'boolean:IS_PROD', // Expects: "true" or "false"
apiSecret: 'base64:API_SECRET', // Expects: base64 encoded string
// Nested structures
database: {
host: 'DB_HOST',
port: 'hard:5432',
credentials: {
user: 'DB_USER',
password: 'base64:DB_PASSWORD_ENCODED',
ssl: 'boolean:DB_USE_SSL'
}
}
},
// Override any env mappings
overwriteObject: {
debugMode: true, // Force debug mode regardless of env
database: {
host: 'localhost' // Force localhost for development
}
}
});
```
### Tools that already use the config feature of npmextra
### Boolean Conversion Rules
- [npmts](https://www.npmjs.com/package/npmts)
- [npmci](https://www.npmjs.com/package/npmci)
- [npmdocker](https://www.npmjs.com/package/npmdocker)
AppData intelligently handles boolean conversions:
## Using the KeyValueStore
1. **String "true"/"false"**: Converted to boolean
2. **With `boolean:` prefix**: Any env var value is converted (`"true"``true`, anything else → `false`)
3. **With `hard_boolean:` prefix**: Hardcoded boolean value
4. **Regular env vars**: Strings remain strings unless prefixed
```typescript
// Environment: FEATURE_A="true", FEATURE_B="yes", FEATURE_C="1"
{
envMapping: {
featureA: 'FEATURE_A', // Result: "true" (string)
featureB: 'boolean:FEATURE_B', // Result: false (only "true" → true)
featureC: 'boolean:FEATURE_C', // Result: false (only "true" → true)
featureD: 'hard_boolean:true' // Result: true (hardcoded)
}
}
```
## Contribution
### Static Helper Functions
We are always happy for code contributions. If you are not the code contributing type that is ok. Still, maintaining Open Source repositories takes considerable time and thought. If you like the quality of what we do and our modules are useful to you we would appreciate a little monthly contribution: You can [contribute one time](https://lossless.link/contribute-onetime) or [contribute monthly](https://lossless.link/contribute). :)
AppData provides convenient static methods for directly accessing and converting environment variables without creating an instance:
For further information read the linked docs at the top of this readme.
```typescript
import { AppData } from '@push.rocks/npmextra';
> MIT licensed | **&copy;** [Lossless GmbH](https://lossless.gmbh)
| By using this npm module you agree to our [privacy policy](https://lossless.gmbH/privacy)
// Get environment variable as boolean
const isEnabled = await AppData.valueAsBoolean('FEATURE_ENABLED');
// Returns: true if "true", false otherwise
[![repo-footer](https://lossless.gitlab.io/publicrelations/repofooter.svg)](https://maintainedby.lossless.com)
// Get environment variable as parsed JSON
interface Config {
timeout: number;
retries: number;
}
const config = await AppData.valueAsJson<Config>('SERVICE_CONFIG');
// Returns: Parsed object or undefined
// Get environment variable as base64 decoded string
const secret = await AppData.valueAsBase64('ENCODED_SECRET');
// Returns: Decoded string or undefined
// Get environment variable as string
const apiUrl = await AppData.valueAsString('API_URL');
// Returns: String value or undefined
// Get environment variable as number
const port = await AppData.valueAsNumber('PORT');
// Returns: Number value or undefined
```
These static methods are perfect for:
- Quick environment variable access without setup
- Simple type conversions in utility functions
- One-off configuration checks
- Scenarios where you don't need the full AppData instance
## Advanced Patterns 🎨
### Reactive Configuration
Subscribe to configuration changes:
```typescript
const kvStore = new KeyValueStore<Config>({
typeArg: 'custom',
identityArg: 'myApp'
});
// Subscribe to changes
kvStore.changeSubject.subscribe((newData) => {
console.log('Configuration changed:', newData);
});
// Changes trigger notifications
await kvStore.writeKey('theme', 'dark');
```
### Testing with Ephemeral Storage
Perfect for unit tests - no file system pollution:
```typescript
const testStore = new KeyValueStore<TestData>({
typeArg: 'ephemeral',
identityArg: 'test'
});
// All operations work normally, but nothing persists to disk
await testStore.writeKey('testKey', 'testValue');
```
### Smart Path Resolution
AppData automatically selects the best storage location:
1. Checks for `/app/data` (containerized environments)
2. Falls back to `/data` (alternate container path)
3. Uses `.nogit/appdata` (local development)
```typescript
// Automatic path selection
const appData = await AppData.createAndInit({
// No dirPath specified - smart detection
requiredKeys: ['apiKey']
});
// Or force ephemeral for testing
const testData = await AppData.createAndInit({
ephemeral: true, // No disk persistence
requiredKeys: ['testKey']
});
```
### Waiting for Configuration
Block until required configuration is available:
```typescript
const appData = await AppData.createAndInit<Config>({
requiredKeys: ['apiKey', 'apiUrl']
});
// Wait for specific key
const apiKey = await appData.waitForAndGetKey('apiKey');
// Check missing keys
const missingKeys = await appData.logMissingKeys();
// Logs: "The following mandatory keys are missing in the appdata:
// -> apiKey,
// -> apiUrl"
```
## Real-World Example 🌍
Here's a complete example of a CLI tool using npmextra:
```typescript
import { Npmextra, AppData, KeyValueStore } from '@push.rocks/npmextra';
interface CliConfig {
githubToken: string;
openaiKey: string;
model: 'gpt-3' | 'gpt-4';
cache: {
enabled: boolean;
ttl: number;
};
}
class MyCLI {
private npmextra: Npmextra;
private appData: AppData<CliConfig>;
private cache: KeyValueStore<{[key: string]: any}>;
async initialize() {
// Load tool configuration
this.npmextra = new Npmextra();
const config = this.npmextra.dataFor<any>('mycli', {
defaultModel: 'gpt-3'
});
// Setup app data with env mapping
this.appData = await AppData.createAndInit<CliConfig>({
requiredKeys: ['githubToken', 'openaiKey'],
envMapping: {
githubToken: 'GITHUB_TOKEN',
openaiKey: 'base64:OPENAI_KEY_ENCODED',
model: 'hard:gpt-4',
cache: {
enabled: 'boolean:ENABLE_CACHE',
ttl: 'hard:3600'
}
}
});
// Initialize cache
this.cache = new KeyValueStore({
typeArg: 'userHomeDir',
identityArg: 'mycli-cache'
});
// Check for missing configuration
const missingKeys = await this.appData.logMissingKeys();
if (missingKeys.length > 0) {
console.error('Please configure the missing keys');
process.exit(1);
}
}
async run() {
await this.initialize();
const config = await this.appData.getKvStore();
const settings = await config.readAll();
console.log(`Using model: ${settings.model}`);
console.log(`Cache enabled: ${settings.cache.enabled}`);
// Use the configuration...
}
}
// Run the CLI
const cli = new MyCLI();
cli.run();
```
## API Reference 📚
### Npmextra Class
```typescript
new Npmextra(cwdArg?: string)
```
- `cwdArg`: Optional working directory path
**Methods:**
- `dataFor<T>(toolName: string, defaultOptions: T): T` - Get merged configuration
### KeyValueStore Class
```typescript
new KeyValueStore<T>(options: {
typeArg: 'custom' | 'userHomeDir' | 'ephemeral';
identityArg: string;
customPath?: string;
mandatoryKeys?: Array<keyof T>;
})
```
**Methods:**
- `readKey(key: string): Promise<T>` - Read single value
- `writeKey(key: string, value: T): Promise<void>` - Write single value
- `readAll(): Promise<T>` - Read all values
- `writeAll(data: T): Promise<void>` - Write all values
- `deleteKey(key: string): Promise<void>` - Delete a key
- `getMissingMandatoryKeys(): Promise<string[]>` - Check missing required keys
- `waitForKeysPresent(keys: string[]): Promise<void>` - Wait for keys
### AppData Class
```typescript
await AppData.createAndInit<T>(options: {
dirPath?: string;
requiredKeys?: Array<keyof T>;
ephemeral?: boolean;
envMapping?: PartialDeep<T>;
overwriteObject?: PartialDeep<T>;
})
```
**Methods:**
- `getKvStore(): Promise<KeyValueStore<T>>` - Get underlying store
- `logMissingKeys(): Promise<Array<keyof T>>` - Log and return missing keys
- `waitForAndGetKey<K>(key: K): Promise<T[K]>` - Wait for and retrieve key
## License and Legal Information
This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository.
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
### Trademarks
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.
### Company Information
Task Venture Capital GmbH
Registered at District court Bremen HRB 35230 HB, Germany
For any legal inquiries or if you require further information, please contact us via email at hello@task.vc.
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.

225
readme.plan.md Normal file
View File

@@ -0,0 +1,225 @@
# AppData Refactoring Plan
## Overview
Refactor the AppData class to improve elegance, maintainability, and extensibility while maintaining 100% backward compatibility.
## Current Issues
- 100+ lines of nested switch statements in processEnvMapping
- Static helpers recreate Qenv instances on every call
- Complex boolean conversion logic scattered across multiple places
- Typo: "ephermal" should be "ephemeral"
- Difficult to test and extend with new transformations
## Architecture Improvements
### 1. Singleton Qenv Provider
Create a shared Qenv instance to avoid repeated instantiation:
```typescript
let sharedQenv: plugins.qenv.Qenv | undefined;
function getQenv(): plugins.qenv.Qenv {
if (!sharedQenv) {
sharedQenv = new plugins.qenv.Qenv(
process.cwd(),
plugins.path.join(process.cwd(), '.nogit')
);
}
return sharedQenv;
}
```
### 2. Centralized Type Converters
Extract all conversion logic into pure utility functions:
```typescript
function toBoolean(value: unknown): boolean {
if (typeof value === 'boolean') return value;
if (value == null) return false;
const s = String(value).toLowerCase();
return s === 'true';
}
function toJson<T>(value: unknown): T | undefined {
if (typeof value === 'string') {
try {
return JSON.parse(value);
} catch {
return undefined;
}
}
return value as T;
}
function fromBase64(value: unknown): string {
if (value == null) return '';
return Buffer.from(String(value), 'base64').toString('utf8');
}
function toNumber(value: unknown): number | undefined {
if (value == null) return undefined;
const num = Number(value);
return Number.isNaN(num) ? undefined : num;
}
function toString(value: unknown): string | undefined {
if (value == null) return undefined;
return String(value);
}
```
### 3. Declarative Pipeline Architecture
Replace the giant switch statement with a composable pipeline:
#### Data Structures
```typescript
type MappingSpec = {
source:
| { type: 'env', key: string }
| { type: 'hard', value: string };
transforms: Transform[];
}
type Transform = 'boolean' | 'json' | 'base64' | 'number';
```
#### Pipeline Functions
```typescript
// Parse mapping string into spec
function parseMappingSpec(input: string): MappingSpec
// Resolve the source value
async function resolveSource(source: MappingSpec['source']): Promise<unknown>
// Apply transformations
function applyTransforms(value: unknown, transforms: Transform[]): unknown
// Complete pipeline
async function processMappingValue(mappingString: string): Promise<unknown>
```
### 4. Transform Registry
Enable easy extension with new transforms:
```typescript
const transformRegistry: Record<string, (v: unknown) => unknown> = {
boolean: toBoolean,
json: toJson,
base64: fromBase64,
number: toNumber,
};
```
### 5. Simplified processEnvMapping
Build pure object tree first, then write to kvStore:
```typescript
async function evaluateMappingValue(mappingValue: any): Promise<any> {
if (typeof mappingValue === 'string') {
return processMappingValue(mappingValue);
}
if (mappingValue && typeof mappingValue === 'object') {
const out: any = {};
for (const [k, v] of Object.entries(mappingValue)) {
out[k] = await evaluateMappingValue(v);
}
return out;
}
return undefined;
}
// Main loop becomes:
for (const key in this.options.envMapping) {
const evaluated = await evaluateMappingValue(this.options.envMapping[key]);
if (evaluated !== undefined) {
await this.kvStore.writeKey(key as keyof T, evaluated);
}
}
```
## Backward Compatibility
### Supported Prefixes (Maintained)
- `hard:` - Hardcoded value
- `hard_boolean:` - Hardcoded boolean
- `hard_json:` - Hardcoded JSON
- `hard_base64:` - Hardcoded base64
- `boolean:` - Environment variable as boolean
- `json:` - Environment variable as JSON
- `base64:` - Environment variable as base64
### Supported Suffixes (Maintained)
- `_JSON` - Auto-parse as JSON
- `_BASE64` - Auto-decode from base64
### Typo Fix Strategy
- Add `ephemeral` option to interface
- Keep reading `ephermal` for backward compatibility
- Log deprecation warning when old spelling is used
## Implementation Steps
1. **Add utility functions** at the top of the file
2. **Implement pipeline functions** (parseMappingSpec, resolveSource, applyTransforms)
3. **Refactor processEnvMapping** to use the pipeline
4. **Update static helpers** to use shared utilities
5. **Fix typo** with compatibility shim
6. **Add error boundaries** for better error reporting
7. **Test** to ensure backward compatibility
## Benefits
### Code Quality
- **70% reduction** in processEnvMapping complexity
- **Better separation** of concerns
- **Easier testing** - each function is pure and testable
- **Cleaner error handling** with boundaries
### Performance
- **Shared Qenv instance** reduces allocations
- **Optional parallelization** with Promise.all
- **Fewer repeated operations**
### Maintainability
- **Extensible** - Easy to add new transforms
- **Readable** - Clear pipeline flow
- **Debuggable** - Each step can be logged
- **Type-safe** - Better TypeScript support
## Testing Strategy
1. **Unit tests** for each utility function
2. **Integration tests** for the full pipeline
3. **Backward compatibility tests** for all existing prefixes/suffixes
4. **Edge case tests** for error conditions
## Future Extensions
With the transform registry, adding new features becomes trivial:
```typescript
// Add YAML support
transformRegistry['yaml'] = (v) => YAML.parse(String(v));
// Add integer parsing
transformRegistry['int'] = (v) => parseInt(String(v), 10);
// Add custom transformers
transformRegistry['uppercase'] = (v) => String(v).toUpperCase();
```
## Migration Path
1. Implement new architecture alongside existing code
2. Gradually migrate internal usage
3. Mark old patterns as deprecated (with warnings)
4. Remove deprecated code in next major version
## Success Metrics
- All existing tests pass
- No breaking changes for users
- Reduced code complexity (measurable via cyclomatic complexity)
- Improved test coverage
- Better performance (fewer allocations, optional parallelization)

View File

@@ -1,5 +1,5 @@
{
"testTool":{
"testValue":2
}
}
"testTool": {
"testValue": 2
}
}

3
test/somekv.json Normal file
View File

@@ -0,0 +1,3 @@
{
"myKey": "myValue"
}

26
test/test.appdata.ts Normal file
View File

@@ -0,0 +1,26 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
// module to test
import * as npmextra from '../ts/index.js';
interface ITestOptions {
hi: string;
deep: {
deep1: string;
deep2: string;
};
}
let testAppdata: npmextra.AppData<ITestOptions>;
tap.test('should create a valid AppData', async () => {
testAppdata = new npmextra.AppData<ITestOptions>({
envMapping: {
deep: {
deep1: '',
},
},
});
});
export default tap.start();

139
test/test.boolean-false.ts Normal file
View File

@@ -0,0 +1,139 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import * as npmextra from '../ts/index.js';
// Test environment variable with boolean false value
tap.test('should handle boolean false values in nested objects correctly', async () => {
// Set up test environment variable
process.env['S3_USESSL'] = 'false';
process.env['S3_ENDPOINT'] = 'https://s3.example.com';
process.env['S3_REGION'] = 'us-east-1';
process.env['S3_ACCESSKEY'] = 'test-key';
process.env['S3_ACCESSSECRET'] = 'test-secret';
// Create AppData with nested object structure similar to CloudlyConfig
const appData = await npmextra.AppData.createAndInit({
ephemeral: true, // Use in-memory storage for testing
envMapping: {
s3Descriptor: {
endpoint: 'S3_ENDPOINT',
region: 'S3_REGION',
accessKey: 'S3_ACCESSKEY',
accessSecret: 'S3_ACCESSSECRET',
useSsl: 'boolean:S3_USESSL'
}
}
});
// Get the kvStore and read the configuration
const kvStore = await appData.getKvStore();
const s3Descriptor = await kvStore.readKey('s3Descriptor');
console.log('\n=== Test Results ===');
console.log('S3 Descriptor:', JSON.stringify(s3Descriptor, null, 2));
console.log('useSsl value:', s3Descriptor?.useSsl);
console.log('useSsl type:', typeof s3Descriptor?.useSsl);
console.log('useSsl === false:', (s3Descriptor?.useSsl as any) === false);
// Verify the values
expect(s3Descriptor).toBeTruthy();
expect(s3Descriptor.endpoint).toEqual('https://s3.example.com');
expect(s3Descriptor.region).toEqual('us-east-1');
expect(s3Descriptor.accessKey).toEqual('test-key');
expect(s3Descriptor.accessSecret).toEqual('test-secret');
// Critical test: useSsl should be false, not undefined
expect(s3Descriptor.useSsl).toEqual(false);
expect(typeof s3Descriptor.useSsl).toEqual('boolean');
expect(s3Descriptor.useSsl).not.toBeUndefined();
});
tap.test('should handle various boolean representations correctly', async () => {
// Test different boolean representations
const testCases = [
{ env: 'false', expected: false },
{ env: 'FALSE', expected: false },
{ env: '0', expected: false },
{ env: 'no', expected: false },
{ env: 'NO', expected: false },
{ env: 'n', expected: false },
{ env: 'off', expected: false },
{ env: 'true', expected: true },
{ env: 'TRUE', expected: true },
{ env: '1', expected: true },
{ env: 'yes', expected: true },
{ env: 'YES', expected: true },
{ env: 'y', expected: true },
{ env: 'on', expected: true },
];
for (const testCase of testCases) {
process.env['TEST_BOOL'] = testCase.env;
const appData = await npmextra.AppData.createAndInit({
ephemeral: true,
envMapping: {
testBool: 'boolean:TEST_BOOL'
}
});
const kvStore = await appData.getKvStore();
const testBool = await kvStore.readKey('testBool');
console.log(`Input "${testCase.env}" => ${testBool} (expected: ${testCase.expected})`);
expect(testBool).toEqual(testCase.expected);
}
});
tap.test('should handle hardcoded boolean false values', async () => {
// Test with hardcoded boolean false value
const appData = await npmextra.AppData.createAndInit({
ephemeral: true,
envMapping: {
boolValue: 'hard_boolean:false'
}
});
const kvStore = await appData.getKvStore();
const boolValue = await kvStore.readKey('boolValue');
console.log('\n=== Hardcoded Boolean Test ===');
console.log('boolValue:', boolValue);
console.log('type:', typeof boolValue);
console.log('is false:', (boolValue as any) === false);
expect(boolValue).toEqual(false);
expect(typeof boolValue).toEqual('boolean');
});
tap.test('should not filter out other falsy values', async () => {
process.env['ZERO_VALUE'] = '0';
process.env['EMPTY_STRING'] = ''; // This should be preserved as empty string
const appData = await npmextra.AppData.createAndInit({
ephemeral: true,
envMapping: {
nested: {
zeroAsNumber: 'ZERO_VALUE', // Should preserve "0" as string
zeroAsBoolean: 'boolean:ZERO_VALUE', // Should convert to false
emptyString: 'EMPTY_STRING', // Should preserve empty string
hardcodedFalse: 'hard_boolean:false', // Should be false
hardcodedZero: 'hard:0', // Should be "0" string
}
}
});
const kvStore = await appData.getKvStore();
const nested = await kvStore.readKey('nested');
console.log('\n=== Falsy Values Test ===');
console.log('nested:', JSON.stringify(nested, null, 2));
expect(nested).toBeTruthy();
expect(nested.zeroAsNumber).toEqual('0');
expect(nested.zeroAsBoolean).toEqual(false);
expect(nested.emptyString).toEqual('');
expect(nested.hardcodedFalse).toEqual(false);
expect(nested.hardcodedZero).toEqual('0');
});
export default tap.start();

View File

@@ -1,25 +1,32 @@
import { expect, tap } from '@pushrocks/tapbundle';
import { expect, tap } from '@git.zone/tstest/tapbundle';
import * as npmextra from '../ts/index';
import * as npmextra from '../ts/index.js';
let myKeyValueStore: npmextra.KeyValueStore;
let myKeyValueStore: npmextra.KeyValueStore<any>;
tap.test('should create a keyValueStore', async () => {
myKeyValueStore = new npmextra.KeyValueStore('custom', 'test');
expect(myKeyValueStore).to.be.instanceof(npmextra.KeyValueStore);
myKeyValueStore = new npmextra.KeyValueStore<any>({
typeArg: 'custom',
identityArg: 'test',
customPath: 'test/somekv.json',
});
expect(myKeyValueStore).toBeInstanceOf(npmextra.KeyValueStore);
});
tap.test('should reset the keyValueStore', async () => {
await myKeyValueStore.reset();
});
tap.test('expect result to be empty', async () => {
let result = myKeyValueStore.readAll();
// tslint:disable-next-line:no-unused-expression
expect(result).to.be.empty;
let result = await myKeyValueStore.readAll();
expect(JSON.stringify(result)).toEqual('{}');
});
tap.test('expect to add an object to the kv Store', async () => {
await myKeyValueStore.writeAll({
myKey: 'myValue',
});
await expect(myKeyValueStore.readKey('myKey')).to.eventually.equal('myValue');
await expect(await myKeyValueStore.readKey('myKey')).toEqual('myValue');
});
tap.start();
export default tap.start();

View File

@@ -1,33 +1,35 @@
import { expect, tap } from '@pushrocks/tapbundle';
import path = require('path');
import { expect, tap } from '@git.zone/tstest/tapbundle';
// module to test
import npmExtra = require('../ts/index');
import * as npmextra from '../ts/index.js';
let testNpmextra: npmExtra.Npmextra;
let testNpmextra: npmextra.Npmextra;
tap.test('should create a new Npmtextra instance', async () => {
testNpmextra = new npmExtra.Npmextra('./test/');
expect(testNpmextra).be.instanceof(npmExtra.Npmextra);
testNpmextra = new npmextra.Npmextra('./test/');
expect(testNpmextra).toBeInstanceOf(npmextra.Npmextra);
});
tap.test('should state wether a npmextra.json exists', async () => {
// tslint:disable-next-line:no-unused-expression
expect(testNpmextra.npmextraJsonExists).be.true;
expect(testNpmextra.npmextraJsonExists).toBeTrue();
});
tap.test('should pass through default value, if not overriden by config from file', async () => {
let testData = testNpmextra.dataFor('testTool', { someKey2: 'someValue2' });
console.log(testData);
expect(testData).have.ownProperty('someKey2');
});
tap.test(
'should pass through default value, if not overriden by config from file',
async () => {
let testData = testNpmextra.dataFor('testTool', { someKey2: 'someValue2' });
console.log(testData);
expect(testData).toHaveProperty('someKey2');
},
);
tap.test('should read a config file', async () => {
let testData = testNpmextra.dataFor<any>('testTool', {
someKey2: 'someValue2',
});
expect(testData).have.ownProperty('someKey2');
expect(testData.testValue).equal(2);
expect(testData).toHaveProperty('someKey2');
expect(testData.testValue).toEqual(2);
});
tap.start();

8
ts/00_commitinfo_data.ts Normal file
View File

@@ -0,0 +1,8 @@
/**
* autocreated commitinfo by @push.rocks/commitinfo
*/
export const commitinfo = {
name: '@push.rocks/npmextra',
version: '5.3.1',
description: 'A utility to enhance npm with additional configuration, tool management capabilities, and a key-value store for project setups.'
}

View File

@@ -1,4 +1,3 @@
import * as plugins from './npmextra.plugins';
export * from './npmextra.classes.npmextra';
export * from './npmextra.classes.keyvaluestore';
export * from './npmextra.classes.appdata.js';
export * from './npmextra.classes.keyvaluestore.js';
export * from './npmextra.classes.npmextra.js';

View File

@@ -0,0 +1,503 @@
import * as plugins from './npmextra.plugins.js';
import { KeyValueStore } from './npmextra.classes.keyvaluestore.js';
// ============================================================================
// Singleton Qenv Provider
// ============================================================================
let sharedQenv: plugins.qenv.Qenv | undefined;
function getQenv(): plugins.qenv.Qenv {
if (!sharedQenv) {
sharedQenv = new plugins.qenv.Qenv(
process.cwd(),
plugins.path.join(process.cwd(), '.nogit')
);
}
return sharedQenv;
}
// ============================================================================
// Type Converters - Centralized conversion logic
// ============================================================================
function toBoolean(value: unknown): boolean {
// If already boolean, return as-is
if (typeof value === 'boolean') {
console.log(` 🔹 toBoolean: value is already boolean: ${value}`);
return value;
}
// Handle null/undefined
if (value == null) {
console.log(` 🔹 toBoolean: value is null/undefined, returning false`);
return false;
}
// Handle string representations
const s = String(value).toLowerCase().trim();
// True values: "true", "1", "yes", "y", "on"
if (['true', '1', 'yes', 'y', 'on'].includes(s)) {
console.log(` 🔹 toBoolean: converting "${value}" to true`);
return true;
}
// False values: "false", "0", "no", "n", "off"
if (['false', '0', 'no', 'n', 'off'].includes(s)) {
console.log(` 🔹 toBoolean: converting "${value}" to false`);
return false;
}
// Default: non-empty string = true, empty = false
const result = s.length > 0;
console.log(` 🔹 toBoolean: defaulting "${value}" to ${result}`);
return result;
}
function toJson<T = any>(value: unknown): T | undefined {
if (value == null) return undefined;
if (typeof value === 'string') {
try {
return JSON.parse(value);
} catch {
return undefined;
}
}
return value as T;
}
function fromBase64(value: unknown): string | undefined {
if (value == null) return undefined;
try {
return Buffer.from(String(value), 'base64').toString('utf8');
} catch {
return String(value);
}
}
function toNumber(value: unknown): number | undefined {
if (value == null) return undefined;
const num = Number(value);
return Number.isNaN(num) ? undefined : num;
}
function toString(value: unknown): string | undefined {
if (value == null) return undefined;
return String(value);
}
// ============================================================================
// Declarative Pipeline Architecture
// ============================================================================
type Transform = 'boolean' | 'json' | 'base64' | 'number';
type MappingSpec = {
source:
| { type: 'env'; key: string }
| { type: 'hard'; value: string };
transforms: Transform[];
};
// Transform registry for extensibility
const transformRegistry: Record<string, (v: unknown) => unknown> = {
boolean: toBoolean,
json: toJson,
base64: fromBase64,
number: toNumber,
};
/**
* Parse a mapping string into a declarative spec
*/
function parseMappingSpec(input: string): MappingSpec {
const transforms: Transform[] = [];
let remaining = input;
// Check for hardcoded prefixes with type conversion
if (remaining.startsWith('hard_boolean:')) {
return {
source: { type: 'hard', value: remaining.slice(13) },
transforms: ['boolean']
};
}
if (remaining.startsWith('hard_json:')) {
return {
source: { type: 'hard', value: remaining.slice(10) },
transforms: ['json']
};
}
if (remaining.startsWith('hard_base64:')) {
return {
source: { type: 'hard', value: remaining.slice(12) },
transforms: ['base64']
};
}
// Check for generic hard: prefix
if (remaining.startsWith('hard:')) {
remaining = remaining.slice(5);
// Check for legacy suffixes on hardcoded values
if (remaining.endsWith('_JSON')) {
transforms.push('json');
remaining = remaining.slice(0, -5);
} else if (remaining.endsWith('_BASE64')) {
transforms.push('base64');
remaining = remaining.slice(0, -7);
}
return {
source: { type: 'hard', value: remaining },
transforms
};
}
// Check for env var prefixes
if (remaining.startsWith('boolean:')) {
transforms.push('boolean');
remaining = remaining.slice(8);
} else if (remaining.startsWith('json:')) {
transforms.push('json');
remaining = remaining.slice(5);
} else if (remaining.startsWith('base64:')) {
transforms.push('base64');
remaining = remaining.slice(7);
}
// Check for legacy suffixes on env vars
if (remaining.endsWith('_JSON')) {
transforms.push('json');
remaining = remaining.slice(0, -5);
} else if (remaining.endsWith('_BASE64')) {
transforms.push('base64');
remaining = remaining.slice(0, -7);
}
return {
source: { type: 'env', key: remaining },
transforms
};
}
/**
* Resolve the source value (env var or hardcoded)
*/
async function resolveSource(source: MappingSpec['source']): Promise<unknown> {
if (source.type === 'hard') {
return source.value;
}
// source.type === 'env'
// Workaround for Qenv bug where empty strings are treated as undefined
// Check process.env directly first to preserve empty strings
if (Object.prototype.hasOwnProperty.call(process.env, source.key)) {
return process.env[source.key];
}
// Fall back to Qenv for other sources (env.json, docker secrets, etc.)
return await getQenv().getEnvVarOnDemand(source.key);
}
/**
* Apply transformations in sequence
*/
function applyTransforms(value: unknown, transforms: Transform[]): unknown {
return transforms.reduce((acc, transform) => {
const fn = transformRegistry[transform];
return fn ? fn(acc) : acc;
}, value);
}
/**
* Process a mapping value through the complete pipeline
*/
async function processMappingValue(mappingString: string): Promise<unknown> {
const spec = parseMappingSpec(mappingString);
console.log(` 🔍 Processing mapping: "${mappingString}"`);
console.log(` Source: ${spec.source.type === 'env' ? `env:${spec.source.key}` : `hard:${spec.source.value}`}`);
console.log(` Transforms: ${spec.transforms.length > 0 ? spec.transforms.join(', ') : 'none'}`);
const rawValue = await resolveSource(spec.source);
console.log(` Raw value: ${JSON.stringify(rawValue)} (type: ${typeof rawValue})`);
if (rawValue === undefined || rawValue === null) {
console.log(` ⚠️ Raw value is undefined/null, returning undefined`);
return undefined;
}
const result = applyTransforms(rawValue, spec.transforms);
console.log(` Final value: ${JSON.stringify(result)} (type: ${typeof result})`);
return result;
}
/**
* Recursively evaluate mapping values (strings or nested objects)
*/
async function evaluateMappingValue(mappingValue: any): Promise<any> {
// Handle null explicitly - it should return null, not be treated as object
if (mappingValue === null) {
console.log(` 📌 Value is null, returning null`);
return null;
}
// Handle strings (mapping specs)
if (typeof mappingValue === 'string') {
return processMappingValue(mappingValue);
}
// Handle objects (but not arrays or null)
if (mappingValue && typeof mappingValue === 'object' && !Array.isArray(mappingValue)) {
console.log(` 📂 Processing nested object with ${Object.keys(mappingValue).length} keys`);
const result: any = {};
for (const [key, value] of Object.entries(mappingValue)) {
console.log(` → Processing nested key "${key}"`);
const evaluated = await evaluateMappingValue(value);
// Important: Don't filter out false or other falsy values!
// Only skip if explicitly undefined
if (evaluated !== undefined) {
result[key] = evaluated;
console.log(` ✓ Nested key "${key}" = ${JSON.stringify(evaluated)} (type: ${typeof evaluated})`);
} else {
console.log(` ⚠️ Nested key "${key}" evaluated to undefined, skipping`);
}
}
return result;
}
// For any other type (numbers, booleans, etc.), return as-is
console.log(` 📎 Returning value as-is: ${JSON.stringify(mappingValue)} (type: ${typeof mappingValue})`);
return mappingValue;
}
// ============================================================================
// AppData Interface and Class
// ============================================================================
export interface IAppDataOptions<T = any> {
dirPath?: string;
requiredKeys?: Array<keyof T>;
/**
* Whether keys should be persisted on disk or not
*/
ephemeral?: boolean;
/**
* @deprecated Use 'ephemeral' instead
*/
ephermal?: boolean;
/**
* kvStoreKey: 'MY_ENV_VAR'
*/
envMapping?: plugins.tsclass.typeFest.PartialDeep<T>;
overwriteObject?: plugins.tsclass.typeFest.PartialDeep<T>;
}
export class AppData<T = any> {
/**
* creates appdata. If no pathArg is given, data will be stored here:
* ${PWD}/.nogit/appdata
* @param pathArg
* @returns
*/
public static async createAndInit<T = any>(
optionsArg: IAppDataOptions<T> = {},
): Promise<AppData<T>> {
const appData = new AppData<T>(optionsArg);
await appData.readyDeferred.promise;
return appData;
}
/**
* Static helper to get an environment variable as a boolean
* @param envVarName The name of the environment variable
* @returns boolean value (true if env var is "true", false otherwise)
*/
public static async valueAsBoolean(envVarName: string): Promise<boolean> {
const value = await getQenv().getEnvVarOnDemand(envVarName);
return toBoolean(value);
}
/**
* Static helper to get an environment variable as parsed JSON
* @param envVarName The name of the environment variable
* @returns Parsed JSON object/array
*/
public static async valueAsJson<R = any>(envVarName: string): Promise<R | undefined> {
const value = await getQenv().getEnvVarOnDemand(envVarName);
return toJson<R>(value);
}
/**
* Static helper to get an environment variable as base64 decoded string
* @param envVarName The name of the environment variable
* @returns Decoded string
*/
public static async valueAsBase64(envVarName: string): Promise<string | undefined> {
const value = await getQenv().getEnvVarOnDemand(envVarName);
return fromBase64(value);
}
/**
* Static helper to get an environment variable as a string
* @param envVarName The name of the environment variable
* @returns String value
*/
public static async valueAsString(envVarName: string): Promise<string | undefined> {
const value = await getQenv().getEnvVarOnDemand(envVarName);
return toString(value);
}
/**
* Static helper to get an environment variable as a number
* @param envVarName The name of the environment variable
* @returns Number value
*/
public static async valueAsNumber(envVarName: string): Promise<number | undefined> {
const value = await getQenv().getEnvVarOnDemand(envVarName);
return toNumber(value);
}
// instance
public readyDeferred = plugins.smartpromise.defer<void>();
public options: IAppDataOptions<T>;
private kvStore: KeyValueStore<T>;
constructor(optionsArg: IAppDataOptions<T> = {}) {
this.options = optionsArg;
this.init();
}
/**
* inits app data
*/
private async init() {
console.log('🚀 Initializing AppData...');
// Handle backward compatibility for typo
const isEphemeral = this.options.ephemeral ?? this.options.ephermal ?? false;
if (this.options.ephermal && !this.options.ephemeral) {
console.warn('⚠️ Option "ephermal" is deprecated, use "ephemeral" instead.');
}
if (this.options.dirPath) {
console.log(` 📁 Using custom directory: ${this.options.dirPath}`);
} else if (isEphemeral) {
console.log(` 💨 Using ephemeral storage (in-memory only)`);
} else {
const appDataDir = '/app/data';
const dataDir = '/data';
const nogitAppData = '.nogit/appdata';
const appDataExists = plugins.smartfile.fs.isDirectory(appDataDir);
const dataExists = plugins.smartfile.fs.isDirectory(dataDir);
if (appDataExists) {
this.options.dirPath = appDataDir;
console.log(` 📁 Auto-selected container directory: ${appDataDir}`);
} else if (dataExists) {
this.options.dirPath = dataDir;
console.log(` 📁 Auto-selected data directory: ${dataDir}`);
} else {
await plugins.smartfile.fs.ensureDir(nogitAppData);
this.options.dirPath = nogitAppData;
console.log(` 📁 Auto-selected local directory: ${nogitAppData}`);
}
}
this.kvStore = new KeyValueStore<T>({
typeArg: isEphemeral ? 'ephemeral' : 'custom',
identityArg: 'appkv',
customPath: this.options.dirPath,
mandatoryKeys: this.options.requiredKeys as Array<keyof T>,
});
if (this.options.envMapping) {
console.log(`📦 Processing envMapping for AppData...`);
const totalKeys = Object.keys(this.options.envMapping).length;
let processedCount = 0;
// Process each top-level key in envMapping
for (const key in this.options.envMapping) {
try {
const mappingSpec = this.options.envMapping[key];
const specType = mappingSpec === null ? 'null' :
typeof mappingSpec === 'string' ? mappingSpec :
typeof mappingSpec === 'object' ? 'nested object' :
typeof mappingSpec;
console.log(` → Processing key "${key}" with spec: ${specType}`);
const evaluated = await evaluateMappingValue(mappingSpec);
// Important: Don't skip false, 0, empty string, or null values!
// Only skip if explicitly undefined
if (evaluated !== undefined) {
await this.kvStore.writeKey(key as keyof T, evaluated);
processedCount++;
const valueType = evaluated === null ? 'null' :
Array.isArray(evaluated) ? 'array' :
typeof evaluated;
const valuePreview = evaluated === null ? 'null' :
typeof evaluated === 'object' ?
(Array.isArray(evaluated) ? `[${evaluated.length} items]` : `{${Object.keys(evaluated).length} keys}`) :
JSON.stringify(evaluated);
console.log(` ✅ Successfully processed key "${key}" = ${valuePreview} (type: ${valueType})`);
} else {
console.log(` ⚠️ Key "${key}" evaluated to undefined, skipping`);
}
} catch (err) {
console.error(` ❌ Failed to evaluate envMapping for key "${key}":`, err);
}
}
console.log(`📊 EnvMapping complete: ${processedCount}/${totalKeys} keys successfully processed`);
}
// Apply overwrite object after env mapping
if (this.options.overwriteObject) {
const overwriteKeys = Object.keys(this.options.overwriteObject);
console.log(`🔄 Applying overwriteObject with ${overwriteKeys.length} key(s)...`);
for (const key of overwriteKeys) {
const value = this.options.overwriteObject[key];
const valueType = Array.isArray(value) ? 'array' : typeof value;
console.log(` 🔧 Overwriting key "${key}" with ${valueType} value`);
await this.kvStore.writeKey(
key as keyof T,
value,
);
}
console.log(`✅ OverwriteObject complete: ${overwriteKeys.length} key(s) overwritten`);
}
this.readyDeferred.resolve();
console.log('✨ AppData initialization complete!');
}
/**
* returns a kvstore that resides in appdata
*/
public async getKvStore(): Promise<KeyValueStore<T>> {
await this.readyDeferred.promise;
return this.kvStore;
}
public async logMissingKeys(): Promise<Array<keyof T>> {
const kvStore = await this.getKvStore();
const missingMandatoryKeys = await kvStore.getMissingMandatoryKeys();
if (missingMandatoryKeys.length > 0) {
console.log(
`The following mandatory keys are missing in the appdata:\n -> ${missingMandatoryKeys.join(
',\n -> ',
)}`,
);
} else {
console.log('All mandatory keys are present in the appdata');
}
return missingMandatoryKeys;
}
public async waitForAndGetKey<K extends keyof T>(
keyArg: K,
): Promise<T[K] | undefined> {
await this.readyDeferred.promise;
await this.kvStore.waitForKeysPresent([keyArg]);
return this.kvStore.readKey(keyArg);
}
}

View File

@@ -1,73 +1,125 @@
import * as plugins from './npmextra.plugins';
import * as paths from './npmextra.paths';
import * as plugins from './npmextra.plugins.js';
import * as paths from './npmextra.paths.js';
import { Task } from '@pushrocks/taskbuffer';
import { Task } from '@push.rocks/taskbuffer';
export type TKeyValueStore = 'path' | 'gitProject' | 'custom';
export type TKeyValueStore = 'custom' | 'userHomeDir' | 'ephemeral';
export interface IKvStoreConstructorOptions<T> {
typeArg: TKeyValueStore;
identityArg: string;
customPath?: string;
mandatoryKeys?: Array<keyof T>;
}
/**
* kvStore is a simple key vlaue store to store data about projects between runs
* kvStore is a simple key value store to store data about projects between runs
*/
export class KeyValueStore {
private dataObject: any = {};
private deletedObject: any = {};
export class KeyValueStore<T = any> {
private dataObject: Partial<T> = {};
private deletedObject: Partial<T> = {};
private mandatoryKeys: Set<keyof T> = new Set();
public changeSubject = new plugins.smartrx.rxjs.Subject<Partial<T>>();
private storedStateString: string = '';
public syncTask = new Task({
name: 'syncTask',
buffered: true,
bufferMax: 1,
execDelay: 0,
taskFunction: async () => {
this.dataObject = {
...plugins.smartfile.fs.toObjectSync(this.filePath),
...this.dataObject,
};
for (const key of Object.keys(this.deletedObject)) {
delete this.dataObject[key];
if (this.type !== 'ephemeral') {
this.dataObject = {
...plugins.smartfile.fs.toObjectSync(this.filePath),
...this.dataObject,
};
for (const key of Object.keys(this.deletedObject) as Array<keyof T>) {
delete this.dataObject[key];
}
this.deletedObject = {};
await plugins.smartfile.memory.toFs(
plugins.smartjson.stringifyPretty(this.dataObject),
this.filePath,
);
}
const newStateString = plugins.smartjson.stringify(this.dataObject);
// change detection
if (newStateString !== this.storedStateString) {
this.storedStateString = newStateString;
this.changeSubject.next(this.dataObject);
}
this.deletedObject = {};
await plugins.smartfile.memory.toFs(JSON.stringify(this.dataObject), this.filePath);
},
});
/**
* computes the identity
* computes the identity and filePath
*/
private initFilePath = () => {
// determine the right base directory
if (this.type === 'ephemeral') {
// No file path is needed for ephemeral type
return;
}
if (this.customPath) {
// Use custom path if provided
const absolutePath = plugins.smartpath.transform.makeAbsolute(
this.customPath,
paths.cwd,
);
this.filePath = absolutePath;
if (plugins.smartfile.fs.isDirectorySync(this.filePath)) {
this.filePath = plugins.path.join(
this.filePath,
this.identity + '.json',
);
}
plugins.smartfile.fs.ensureFileSync(this.filePath, '{}');
return;
}
let baseDir: string;
if (this.type === 'custom') {
baseDir = paths.kvCustomDir;
} else if (this.type === 'gitProject') {
baseDir = paths.kvGitDir;
} else if (this.type === 'path') {
baseDir = paths.kvPathDir;
if (this.type === 'userHomeDir') {
baseDir = paths.kvUserHomeDirBase;
} else {
throw new Error('kv type not supported');
}
this.filePath = plugins.path.join(baseDir, this.identity + '.json');
plugins.smartfile.fs.ensureDirSync(paths.kvCustomDir);
plugins.smartfile.fs.ensureDirSync(paths.kvGitDir);
plugins.smartfile.fs.ensureDirSync(paths.kvPathDir);
plugins.smartfile.fs.ensureDirSync(baseDir);
plugins.smartfile.fs.ensureFileSync(this.filePath, '{}');
};
public type: TKeyValueStore; // the type of the kvStore
public identity: string; // the identity of the kvStore
public filePath: string; // the filePath of the kvStore
// if no custom path is provided, try to store at home directory
public type: TKeyValueStore;
public identity: string;
public filePath?: string;
private customPath?: string; // Optionally allow custom path
/**
* the constructor of keyvalue store
* @param typeArg
* @param customStringArg
* @param identityArg
* @param customPath Optional custom path for the keyValue store
*/
constructor(typeArg: TKeyValueStore, customStringArg: string) {
// set kvStoreType
this.type = typeArg;
this.identity = customStringArg;
constructor(optionsArg: IKvStoreConstructorOptions<T>) {
if (optionsArg.customPath && optionsArg.typeArg !== 'custom') {
throw new Error('customPath can only be provided if typeArg is custom');
}
if (optionsArg.typeArg === 'custom' && !optionsArg.customPath) {
throw new Error('customPath must be provided if typeArg is custom');
}
this.type = optionsArg.typeArg;
this.identity = optionsArg.identityArg;
this.customPath = optionsArg.customPath; // Store custom path if provided
this.initFilePath();
if (optionsArg.mandatoryKeys) {
this.setMandatoryKeys(optionsArg.mandatoryKeys);
}
}
/**
* reads all keyValue pairs at once and returns them
*/
public async readAll() {
public async readAll(): Promise<Partial<T>> {
await this.syncTask.trigger();
return this.dataObject;
}
@@ -75,21 +127,24 @@ export class KeyValueStore {
/**
* reads a keyValueFile from disk
*/
public async readKey(keyArg: string) {
public async readKey<K extends keyof T>(keyArg: K): Promise<T[K]> {
await this.syncTask.trigger();
return this.dataObject[keyArg];
return this.dataObject[keyArg] as T[K];
}
/**
* writes a specific key to the keyValueStore
*/
public async writeKey(keyArg: string, valueArg: any) {
public async writeKey<K extends keyof T>(
keyArg: K,
valueArg: T[K],
): Promise<void> {
await this.writeAll({
[keyArg]: valueArg,
});
} as unknown as Partial<T>);
}
public async deleteKey(keyArg: string) {
public async deleteKey<K extends keyof T>(keyArg: K): Promise<void> {
this.deletedObject[keyArg] = this.dataObject[keyArg];
await this.syncTask.trigger();
}
@@ -97,7 +152,7 @@ export class KeyValueStore {
/**
* writes all keyValue pairs in the object argument
*/
public async writeAll(keyValueObject) {
public async writeAll(keyValueObject: Partial<T>): Promise<void> {
this.dataObject = { ...this.dataObject, ...keyValueObject };
await this.syncTask.trigger();
}
@@ -105,8 +160,63 @@ export class KeyValueStore {
/**
* wipes a key value store from disk
*/
public async wipe() {
public async wipe(): Promise<void> {
this.dataObject = {};
await plugins.smartfile.fs.remove(this.filePath);
if (this.type !== 'ephemeral') {
await plugins.smartfile.fs.remove(this.filePath);
}
}
/**
* resets the KeyValueStore to the initial state by syncing first, deleting all keys, and then triggering a sync again
*/
public async reset(): Promise<void> {
await this.syncTask.trigger(); // Sync to get the latest state
// Delete all keys from the dataObject and add them to deletedObject
for (const key of Object.keys(this.dataObject) as Array<keyof T>) {
this.deletedObject[key] = this.dataObject[key];
delete this.dataObject[key];
}
await this.syncTask.trigger(); // Sync again to reflect the deletion
}
private setMandatoryKeys(keys: Array<keyof T>) {
keys.forEach((key) => this.mandatoryKeys.add(key));
}
public async getMissingMandatoryKeys(): Promise<Array<keyof T>> {
await this.readAll();
return Array.from(this.mandatoryKeys).filter(
(key) => !(key in this.dataObject),
);
}
public async waitForKeysPresent<K extends keyof T>(
keysArg: K[],
): Promise<void> {
const missingKeys = keysArg.filter((keyArg) => !this.dataObject[keyArg]);
if (missingKeys.length === 0) {
return;
}
return new Promise<void>((resolve, reject) => {
const subscription = this.changeSubject.subscribe(() => {
const missingKeys = keysArg.filter(
(keyArg) => !this.dataObject[keyArg],
);
if (missingKeys.length === 0) {
subscription.unsubscribe();
resolve();
}
});
});
}
public async waitForAndGetKey<K extends keyof T>(
keyArg: K,
): Promise<T[K] | undefined> {
await this.waitForKeysPresent([keyArg]);
return this.readKey(keyArg);
}
}

View File

@@ -1,5 +1,5 @@
import * as plugins from './npmextra.plugins';
import * as paths from './npmextra.paths';
import * as plugins from './npmextra.plugins.js';
import * as paths from './npmextra.paths.js';
/**
* Npmextra class allows easy configuration of tools
@@ -27,7 +27,10 @@ export class Npmextra {
/**
* merges the supplied options with the ones from npmextra.json
*/
dataFor<IToolConfig>(toolnameArg: string, defaultOptionsArg: any): IToolConfig {
dataFor<IToolConfig>(
toolnameArg: string,
defaultOptionsArg: any,
): IToolConfig {
let npmextraToolOptions;
if (this.npmextraJsonData[toolnameArg]) {
npmextraToolOptions = this.npmextraJsonData[toolnameArg];
@@ -45,7 +48,9 @@ export class Npmextra {
* checks if the JSON exists
*/
private checkNpmextraJsonExists() {
this.npmextraJsonExists = plugins.smartfile.fs.fileExistsSync(this.lookupPath);
this.npmextraJsonExists = plugins.smartfile.fs.fileExistsSync(
this.lookupPath,
);
}
/**
@@ -64,7 +69,9 @@ export class Npmextra {
*/
private checkNpmextraJsonData() {
if (this.npmextraJsonExists) {
this.npmextraJsonData = plugins.smartfile.fs.toObjectSync(this.lookupPath);
this.npmextraJsonData = plugins.smartfile.fs.toObjectSync(
this.lookupPath,
);
} else {
this.npmextraJsonData = {};
}

View File

@@ -1,8 +1,11 @@
import * as plugins from './npmextra.plugins';
import * as plugins from './npmextra.plugins.js';
// directories
export let cwd = process.cwd();
export let packageDir = plugins.path.join(__dirname, '../');
export let packageDir = plugins.path.join(
plugins.smartpath.get.dirnameFromImportMetaUrl(import.meta.url),
'../',
);
// ----------------------
// keyValueStore specific
@@ -13,22 +16,7 @@ export let home = plugins.smartpath.get.home();
/**
* keyValue base path
*/
export let kvBase = plugins.path.join(home, '.npmextra/kv');
/**
* the base directory for custom string based key value store
*/
export let kvCustomDir = plugins.path.join(kvBase, 'custom');
/**
* the subdir for git based keyValue
*/
export let kvGitDir = plugins.path.join(kvBase, 'git');
/**
* keyValue for path based keyValue store
*/
export let kvPathDir = plugins.path.join(kvBase, 'path');
export let kvUserHomeDirBase = plugins.path.join(home, '.npmextra/kv');
// files
export let configFile = plugins.path.join(cwd, 'npmextra.json');

View File

@@ -1,8 +1,25 @@
import * as beautylog from '@pushrocks/smartlog';
import * as path from 'path';
import * as smartfile from '@pushrocks/smartfile';
import * as smartpath from '@pushrocks/smartpath';
import * as smartpromise from '@pushrocks/smartpromise';
import * as taskbuffer from '@pushrocks/taskbuffer';
import * as tsclass from '@tsclass/tsclass';
export { beautylog, path, smartfile, smartpath, smartpromise, taskbuffer };
export { tsclass };
import * as qenv from '@push.rocks/qenv';
import * as smartlog from '@push.rocks/smartlog';
import * as path from 'path';
import * as smartfile from '@push.rocks/smartfile';
import * as smartjson from '@push.rocks/smartjson';
import * as smartpath from '@push.rocks/smartpath';
import * as smartpromise from '@push.rocks/smartpromise';
import * as smartrx from '@push.rocks/smartrx';
import * as taskbuffer from '@push.rocks/taskbuffer';
export {
qenv,
smartlog,
path,
smartfile,
smartjson,
smartpath,
smartpromise,
smartrx,
taskbuffer,
};

15
tsconfig.json Normal file
View File

@@ -0,0 +1,15 @@
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"useDefineForClassFields": false,
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"esModuleInterop": true,
"verbatimModuleSyntax": true,
"baseUrl": ".",
"paths": {}
},
"exclude": ["dist_*/**/*.d.ts"]
}

View File

@@ -1,17 +0,0 @@
{
"extends": ["tslint:latest", "tslint-config-prettier"],
"rules": {
"semicolon": [true, "always"],
"no-console": false,
"ordered-imports": false,
"object-literal-sort-keys": false,
"member-ordering": {
"options":{
"order": [
"static-method"
]
}
}
},
"defaultSeverity": "warning"
}