-
Notifications
You must be signed in to change notification settings - Fork 100
Auto-update: lazy npm self-update in the CLI, git self-update in the skill #16
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,191 @@ | ||
| import { readFileSync, writeFileSync, mkdirSync, existsSync, realpathSync } from 'fs'; | ||
| import { join, dirname } from 'path'; | ||
| import { homedir } from 'os'; | ||
| import fetch from 'node-fetch'; | ||
|
|
||
| const UPDATE_DIR = join(homedir(), '.postiz'); | ||
| const UPDATE_FILE = join(UPDATE_DIR, 'update-check.json'); | ||
| const CHECK_INTERVAL = 2 * 24 * 60 * 60 * 1000; | ||
| const REGISTRY_URL = 'https://registry.npmjs.org/postiz/latest'; | ||
|
|
||
| const currentVersion: string = require('../package.json').version; | ||
|
|
||
| function loadMarker(): { lastCheck: number } | null { | ||
| try { | ||
| if (!existsSync(UPDATE_FILE)) return null; | ||
| const data = JSON.parse(readFileSync(UPDATE_FILE, 'utf-8')); | ||
| if (typeof data.lastCheck !== 'number') return null; | ||
| return data; | ||
| } catch { | ||
| return null; | ||
| } | ||
| } | ||
|
|
||
| function saveMarker(): void { | ||
| try { | ||
| if (!existsSync(UPDATE_DIR)) { | ||
| mkdirSync(UPDATE_DIR, { recursive: true, mode: 0o700 }); | ||
| } | ||
| writeFileSync(UPDATE_FILE, JSON.stringify({ lastCheck: Date.now() }), 'utf-8'); | ||
| } catch { | ||
| // ignore | ||
| } | ||
| } | ||
|
|
||
| type Manager = | ||
| | { kind: 'ephemeral' } | ||
| | { kind: 'auto'; cmd: string; args: string[]; manual: string } | ||
| | { kind: 'local' } | ||
| | { kind: 'unknown' }; | ||
|
|
||
| function detectManager(): Manager { | ||
| let entry: string; | ||
| try { | ||
| entry = realpathSync(process.argv[1]); | ||
| } catch { | ||
| return { kind: 'unknown' }; | ||
| } | ||
| if (entry.includes('/_npx/') || entry.includes('/.bun/install/cache/')) return { kind: 'ephemeral' }; | ||
| const packageRoot = dirname(dirname(entry)); // dist/index.js -> package dir | ||
| if (dirname(packageRoot).endsWith('/lib/node_modules')) | ||
| return { kind: 'auto', cmd: 'npm', args: ['install', '-g', 'postiz@latest'], manual: 'npm install -g postiz@latest' }; | ||
| if (entry.includes('/pnpm/global/') || entry.includes('/pnpm/store/')) | ||
| return { kind: 'auto', cmd: 'pnpm', args: ['add', '-g', 'postiz@latest'], manual: 'pnpm add -g postiz@latest' }; | ||
| if (entry.includes('/yarn/global/')) | ||
| return { kind: 'auto', cmd: 'yarn', args: ['global', 'add', 'postiz@latest'], manual: 'yarn global add postiz@latest' }; | ||
| if (entry.includes('/.bun/install/global/')) | ||
| return { kind: 'auto', cmd: 'bun', args: ['add', '-g', 'postiz@latest'], manual: 'bun add -g postiz@latest' }; | ||
| // Windows paths use backslashes and match none of the above, landing here as 'unknown'. | ||
| if (entry.includes('/node_modules/')) return { kind: 'local' }; | ||
| return { kind: 'unknown' }; | ||
| } | ||
|
|
||
| // Version of the package process.argv[1] resolves to right now (re-resolved so an | ||
| // upgrade that replaced symlinks — e.g. pnpm's store links — is picked up). | ||
| function installedVersionNow(): string | null { | ||
| try { | ||
| const entry = realpathSync(process.argv[1]); | ||
| return JSON.parse(readFileSync(join(dirname(dirname(entry)), 'package.json'), 'utf-8')).version; | ||
| } catch { | ||
| return null; | ||
| } | ||
| } | ||
|
|
||
| // Runs the manager's install and returns a verified re-exec target: [execPath, argv0] | ||
| // for a spawn that is guaranteed to run the new version, or null if the update can't | ||
| // be confirmed. Only ever re-exec a target proven to be `latest`, so a child's own | ||
| // check finds nothing newer and recursion is impossible. | ||
| function runInstall(manager: { cmd: string; args: string[]; manual: string }, latest: string): string[] | null { | ||
| const { spawnSync } = require('child_process'); | ||
| const install = spawnSync(manager.cmd, manager.args, { stdio: ['ignore', 'ignore', 'inherit'], timeout: 120000 }); | ||
| if (install.status !== 0) { | ||
| process.stderr.write(`Update failed. Run manually: ${manager.manual}\n`); | ||
| return null; | ||
| } | ||
| if (installedVersionNow() === latest) return [process.execPath, process.argv[1]]; | ||
| // Some managers (pnpm) relocate the package and repoint the bin instead of | ||
| // updating in place — fall back to the PATH-resolved bin if it is the new version. | ||
| const bin = spawnSync('postiz', ['--version'], { encoding: 'utf-8', timeout: 10000 }); | ||
| if (bin.status === 0 && typeof bin.stdout === 'string' && bin.stdout.trim() === latest) return ['postiz']; | ||
| process.stderr.write(`Update did not apply to this install. Run manually: ${manager.manual}\n`); | ||
| return null; | ||
| } | ||
|
|
||
| async function fetchLatestVersion(): Promise<string | null> { | ||
| try { | ||
| const controller = new AbortController(); | ||
| const timer = setTimeout(() => controller.abort(), 3000); | ||
| try { | ||
| const response = await fetch(REGISTRY_URL, { signal: controller.signal as any }); | ||
| if (!response.ok) return null; | ||
| const data = (await response.json()) as any; | ||
| return typeof data.version === 'string' ? data.version : null; | ||
| } finally { | ||
| clearTimeout(timer); | ||
| } | ||
| } catch { | ||
| return null; | ||
| } | ||
| } | ||
|
|
||
| function isNewer(latest: string, current: string): boolean { | ||
| const a = latest.split('.'); | ||
| const b = current.split('.'); | ||
| if (a.length !== 3 || b.length !== 3) return false; | ||
| for (let i = 0; i < 3; i++) { | ||
| if (!/^\d+$/.test(a[i]) || !/^\d+$/.test(b[i])) return false; | ||
| const diff = parseInt(a[i], 10) - parseInt(b[i], 10); | ||
| if (diff > 0) return true; | ||
| if (diff < 0) return false; | ||
| } | ||
| return false; | ||
| } | ||
|
|
||
| export async function maybeAutoUpdate(argv: string[]): Promise<void> { | ||
| try { | ||
| if (argv.some((a) => ['--version', '-v', '--help', '-h'].includes(a))) return; | ||
| if (argv[0] === 'update') return; | ||
| const marker = loadMarker(); | ||
| if (marker && Date.now() - marker.lastCheck < CHECK_INTERVAL) return; | ||
| const manager = detectManager(); | ||
| if (manager.kind === 'ephemeral') return; | ||
| // Written before the fetch so concurrent commands can't both start a check. | ||
| saveMarker(); | ||
|
Comment on lines
+128
to
+133
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift Serialize the marker check and update attempt. Two CLI processes can both pass Line 129 before either process reaches Line 133. Both processes then query npm and can start a global package update. This breaks the two-day check limit and can create concurrent package-manager operations. Use an atomic cross-process lock. Re-read the marker after acquiring the lock. Release the lock in 🤖 Prompt for AI Agents |
||
| const latest = await fetchLatestVersion(); | ||
| if (!latest || !isNewer(latest, currentVersion)) return; | ||
| // Print recommendations after the command's output so they stay visible. | ||
| if (manager.kind === 'local') { | ||
| process.on('exit', () => | ||
| process.stderr.write(`A newer postiz version (${latest}) is available. This install is project-local — update it in that project's package.json.\n`) | ||
| ); | ||
| return; | ||
| } | ||
| if (manager.kind === 'unknown') { | ||
| process.on('exit', () => process.stderr.write(`A newer postiz version (${latest}) is available.\n`)); | ||
| return; | ||
| } | ||
| const { spawnSync } = require('child_process'); | ||
| process.stderr.write(`Checking for updates... updating ${currentVersion} -> ${latest}\n`); | ||
| const target = runInstall(manager, latest); | ||
| if (!target) return; | ||
| const r = spawnSync(target[0], [...target.slice(1), ...process.argv.slice(2)], { stdio: 'inherit' }); | ||
| if (r.error) return; | ||
| process.exit(r.status ?? 1); | ||
| } catch { | ||
| // never break the user's command | ||
| } | ||
| } | ||
|
|
||
| export async function updateCommand(): Promise<void> { | ||
| try { | ||
| const manager = detectManager(); | ||
| if (manager.kind === 'ephemeral') { | ||
| process.stderr.write('Running via npx — nothing installed to update.\n'); | ||
| return; | ||
| } | ||
| const latest = await fetchLatestVersion(); | ||
| saveMarker(); | ||
| if (!latest) { | ||
| process.stderr.write('Could not reach the npm registry to check for updates.\n'); | ||
| return; | ||
| } | ||
| if (!isNewer(latest, currentVersion)) { | ||
| process.stderr.write(`postiz ${currentVersion} is up to date.\n`); | ||
| return; | ||
| } | ||
| if (manager.kind === 'local') { | ||
| process.stderr.write(`A newer postiz version (${latest}) is available, but this is a project-local install — update it in that project's package.json.\n`); | ||
| return; | ||
| } | ||
| if (manager.kind === 'unknown') { | ||
| process.stderr.write(`A newer postiz version (${latest}) is available. Update it with the package manager it was installed with.\n`); | ||
| return; | ||
| } | ||
| process.stderr.write(`Updating ${currentVersion} -> ${latest}\n`); | ||
| if (runInstall(manager, latest)) { | ||
| process.stderr.write(`Updated to postiz ${latest}.\n`); | ||
| } | ||
| } catch { | ||
| // never fail | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔒 Security & Privacy | 🔴 Critical | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
Repository: gitroomhq/postiz-agent
Length of output: 10713
🌐 Web query:
npm npx documentation -y remote package execution confirmation prompt💡 Result:
When you run a command using npx (or npm exec) that requires a package not currently present in your local dependencies, npm will print a confirmation prompt to the terminal before proceeding with the installation [1][2]. This prompt acts as a security and user-experience measure to prevent the accidental execution of unintended packages [1][2]. To handle this behavior, you can use the following options: --yes (or -y): Automatically accepts the prompt and proceeds with the installation of the requested package [1][2]. --no: Automatically declines the prompt and aborts the installation [1][2]. In environments where standard input is not available (such as non-TTY environments or most CI/CD pipelines), npm automatically assumes --yes, bypassing the interactive prompt [1][3]. Additionally, you can force this behavior by setting the environment variable npm_config_yes=true [3]. Note that some interactive CLI tools may present their own internal prompts after npx finishes installation; these are separate from the npx installation confirmation prompt and cannot be bypassed via npx flags [4].
Citations:
🌐 Web query:
skills CLI documentation gitroomhq/postiz-agent skills add immutable commit SHA integrity verification💡 Result:
The gitroomhq/postiz-agent is a CLI tool designed to allow AI agents (such as Claude or OpenClaw) to automate social media posting [1][2][3]. To install it as a skill for an AI agent, the standard command is: npx skills add gitroomhq/postiz-agent [1][4][3] Regarding your specific query on immutable commit SHA and integrity verification: there is no native CLI feature within the gitroomhq/postiz-agent specifically labeled "immutable commit SHA integrity verification" [1][2][4]. While Postiz documentation references commit SHAs in the context of infrastructure (e.g., Vercel or GitHub Actions injecting VERCEL_GIT_COMMIT_SHA or GITHUB_SHA for tracking release versions or Sentry tags) [5], and security advisories discuss integrity in terms of preventing unauthorized data modification or JWT forgery [6][7][8], these are distinct from a user-facing CLI command for SHA verification. The CLI's security relies on standard authentication mechanisms like OAuth2 device flow or API keys [9][10]. If you are looking for general package integrity, standard npm practices apply (such as verifying package hashes during installation), but this is handled by the Node.js/npm ecosystem rather than a specific command within this agent CLI [1].
Citations:
🏁 Script executed:
Repository: gitroomhq/postiz-agent
Length of output: 22719
🌐 Web query:
vercel-labs skills CLI add repository commit SHA ref syntax💡 Result:
The vercel-labs/skills CLI does not provide a native or explicit command-line flag (such as --ref or
@SHA) to specify a commit SHA or branch ref directly within the npx skills add command syntax [1][2][3]. Under the hood, the CLI's git implementation uses a shallow clone (--depth 1) [4]. While the source code for the CLI includes logic that can handle branch references via a --branch option during the internal git clone process [4], this is not documented or exposed as a standard part of the public-facing CLI interface for users [2][5][6]. Supported repository source formats for the add command include [1][6][3]: - GitHub shorthand (owner/repo) - Full GitHub URLs (e.g., https://github.com/owner/repo) - Git URLs (e.g., git@github.com:owner/repo.git) - GitLab URLs - Local filesystem paths If you need to install skills from a specific commit or branch, the recommended approach is to clone the repository manually to your local filesystem, check out the desired SHA or branch, and then install the skills from that local directory using the CLI [2][6][7]: npx skills add /path/to/your/local/cloned/repo This allows you to point the CLI to a specific version of the skills repository while leveraging the local path installation method [2][8].Citations:
🏁 Script executed:
Repository: gitroomhq/postiz-agent
Length of output: 16946
🌐 Web query:
npm package integrity npx exact package version package-lock integrity verification documentation💡 Result:
npm ensures package integrity primarily through the use of cryptographic hashes stored in the package-lock.json file [1][2][3]. ### Integrity Verification Mechanism When a package is installed, npm downloads the tarball from the registry and records a Standard Subresource Integrity (SRI) string, typically a SHA-512 hash, in the integrity field of the package-lock.json [1][4][3]. Before npm unpacks the downloaded tarball into your node_modules directory, it calculates the hash of the downloaded artifact and compares it against the integrity hash stored in the lockfile [4][3]. If the hashes do not match, the installation process is aborted with an EINTEGRITY error to prevent the use of corrupted or tampered packages [4][3]. This mechanism protects against man-in-the-middle attacks or registry-side tampering after the initial package resolution [4][3]. ### npm vs. npx - npm install / npm ci: When you use standard installation commands, npm relies heavily on package-lock.json to ensure that the exact version and bytes (verified via the integrity hash) are installed across all environments [1][5][6]. npm ci is specifically designed for automated environments; it requires a package-lock.json and fails if the lockfile and package.json are out of sync, ensuring reproducible and secure builds [2][6]. - npx: npx executes packages by temporarily downloading them if they are not already installed [7][6]. Because npx is often used to run commands on-demand, it may not leverage a project's existing package-lock.json in the same strict, authoritative manner as npm install or npm ci [6]. When using npx to run a specific package version (e.g., npx package@version), it fetches the package and validates it according to standard npm registry integrity protocols [7]. ### Best Practices for Integrity To maintain a secure dependency chain: 1. Always commit package-lock.json to version control to lock in integrity hashes [2][6]. 2. Use npm ci in CI/CD pipelines to guarantee that installations strictly match the lockfile [2][6]. 3. For additional security, use community tools (such as lockcheck or lockfile-lint) to audit lockfiles for missing integrity hashes, suspicious registry URLs, or version drift [8][2][3]. 4. Run npm doctor to verify that your local npm environment and cache are healthy, as it can detect if cached package tarballs have become corrupted [9]. Note that the integrity field in package-lock.json refers specifically to the downloaded registry tarball (.tgz), not the final JavaScript code bundled for a browser [3]. Browser-level integrity (Subresource Integrity) must be handled separately by generating SRI hashes for your final, post-build artifacts [3].
Citations:
🏁 Script executed:
Repository: gitroomhq/postiz-agent
Length of output: 364
Pin and verify the updater and skill source.
Line 29 suppresses both
npxandskillsconfirmation prompts. It resolves the unversionedskillspackage and the repository's mutable default branch. A compromised package can execute code as the user, and changed repository content can install malicious guidance globally. Use a locked, auditedskillsversion with a trusted integrity value. Resolve and verify a trusted commit outsideskills add, then install from the verified local checkout because the documented CLI does not expose commit-SHA syntax.🧰 Tools
🪛 SkillSpector (2.5.1)
[warning] 29: [RP1] null: npx commands without a version suffix (e.g.
@1.0.0) create a rug-pull risk if the upstream server is compromised and publishes a malicious update.Remediation: Pin the version: npx
@scope/server@1.2.3(MCP Rug Pull (RP1))
[error] 142: [PE3] Credential Access: Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
Remediation: Remove references to credential paths. Use environment variables or secrets managers. For docs, use placeholder paths (e.g., /path/to/config). Never load .env or token files in production code paths.
(Privilege Escalation (PE3))
[error] 35: [RA1] Self-Modification: Skill modifies its own code, configuration, or behavior at runtime. Self-modification enables an agent to escalate privileges, disable safety constraints, or install persistent backdoors.
Remediation: Prevent the skill from modifying its own code, SKILL.md, or configuration files. Treat skill files as read-only at runtime.
(Rogue Agent (RA1))
🤖 Prompt for AI Agents
Source: Linters/SAST tools