diff --git a/CHANGELOG.md b/CHANGELOG.md index 863faab..031028e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.0.3] - 2026-06-12 + +### Fixed + +- CLI custom actions now resolve user data from `%APPDATA%/ShellForge/shellforge-data/` when `SHELLFORGE_USER_DATA` is not set, fixing profile aliases such as `lancar-horas` on packaged installs. + ## [1.0.2] - 2026-06-12 ### Fixed diff --git a/commands/action-runner/action-runner.js b/commands/action-runner/action-runner.js index 58bb6af..35ee413 100644 --- a/commands/action-runner/action-runner.js +++ b/commands/action-runner/action-runner.js @@ -8,6 +8,7 @@ */ const { getConfigs, logger, getArgValue, consts, createPuppeteerBrowser } = require("../../utils"); +const { resolveUserDataRoot } = require("../../utils/getConfig"); const { normalizeSteps } = require("./normalizeSteps"); const { runSteps } = require("./runSteps"); const fs = require("fs"); @@ -19,12 +20,6 @@ const DEFAULT_VIEWPORT = { width: 1540, height: 700, }; -const RUNTIME_ROOT = path.resolve(__dirname, "../.."); -const USER_DATA_ROOT = - typeof process.env.SHELLFORGE_USER_DATA === "string" && - process.env.SHELLFORGE_USER_DATA.trim().length > 0 - ? path.resolve(process.env.SHELLFORGE_USER_DATA.trim()) - : RUNTIME_ROOT; const PROFILE_BASE_DIRECTORY = ".shellforge-browser-profiles"; const logError = logger("error", consts.identification.actionRunner); @@ -120,7 +115,7 @@ function validateBrowserProfileName(browserProfile, actionName) { function getBrowserLaunchOverrides( actionConfig, actionName, - projectRoot = USER_DATA_ROOT + projectRoot = resolveUserDataRoot() ) { if (!actionConfig || typeof actionConfig !== "object") { return {}; diff --git a/package.json b/package.json index 0eb5a94..892e786 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "shellforge", - "version": "1.0.2", + "version": "1.0.3", "description": "ShellForge - custom Windows PowerShell automation and commands", "license": "ISC", "author": "João Luiz de Castro", diff --git a/scripts/run-core-tests.mjs b/scripts/run-core-tests.mjs index 745baf8..3ce3a2a 100644 --- a/scripts/run-core-tests.mjs +++ b/scripts/run-core-tests.mjs @@ -6,6 +6,7 @@ import { fileURLToPath } from "node:url"; const CORE_TEST_ROOTS = [ "commands/action-runner/tests", "command-lib/tests", + "utils/tests", ]; const repoRootPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); diff --git a/utils/getConfig.js b/utils/getConfig.js index b0fe12f..e50478b 100644 --- a/utils/getConfig.js +++ b/utils/getConfig.js @@ -1,13 +1,46 @@ const fs = require("node:fs"); const path = require("node:path"); +const { getPackagedUserDataRoot } = require("./shellforgePaths"); -function resolveUserDataRoot() { - const userDataRoot = process.env.SHELLFORGE_USER_DATA; - if (typeof userDataRoot === "string" && userDataRoot.trim().length > 0) { - return path.resolve(userDataRoot.trim()); +function getRuntimeRoot() { + return path.resolve(__dirname, ".."); +} + +function hasConfigAtRoot(rootPath) { + const configPath = path.join(rootPath, "config", "config.json"); + return fs.existsSync(configPath); +} + +function resolveUserDataRootFromInputs({ + envUserDataRoot, + runtimeRoot, + appDataPath, +}) { + if (typeof envUserDataRoot === "string" && envUserDataRoot.trim().length > 0) { + return path.resolve(envUserDataRoot.trim()); } - return path.resolve(__dirname, ".."); + const resolvedRuntimeRoot = path.resolve(runtimeRoot); + if (hasConfigAtRoot(resolvedRuntimeRoot)) { + return resolvedRuntimeRoot; + } + + if (typeof appDataPath === "string" && appDataPath.trim().length > 0) { + const packagedUserDataRoot = getPackagedUserDataRoot(appDataPath.trim()); + if (hasConfigAtRoot(packagedUserDataRoot)) { + return packagedUserDataRoot; + } + } + + return resolvedRuntimeRoot; +} + +function resolveUserDataRoot() { + return resolveUserDataRootFromInputs({ + envUserDataRoot: process.env.SHELLFORGE_USER_DATA, + runtimeRoot: getRuntimeRoot(), + appDataPath: process.env.APPDATA, + }); } function readConfigFile(configPath) { @@ -26,4 +59,5 @@ async function getConfigs() { module.exports = { getConfigs, resolveUserDataRoot, + resolveUserDataRootFromInputs, }; diff --git a/utils/shellforgePaths.js b/utils/shellforgePaths.js new file mode 100644 index 0000000..a7d21a0 --- /dev/null +++ b/utils/shellforgePaths.js @@ -0,0 +1,15 @@ +const path = require("node:path"); + +// Keep in sync with ui/src/main/services/shellforgeRuntimeLayout.ts. +const SHELLFORGE_APP_DATA_DIR_NAME = "ShellForge"; +const USER_DATA_REPO_DIR_NAME = "shellforge-data"; + +function getPackagedUserDataRoot(appDataPath) { + return path.join(appDataPath, SHELLFORGE_APP_DATA_DIR_NAME, USER_DATA_REPO_DIR_NAME); +} + +module.exports = { + SHELLFORGE_APP_DATA_DIR_NAME, + USER_DATA_REPO_DIR_NAME, + getPackagedUserDataRoot, +}; diff --git a/utils/tests/getConfig.test.js b/utils/tests/getConfig.test.js new file mode 100644 index 0000000..66a0362 --- /dev/null +++ b/utils/tests/getConfig.test.js @@ -0,0 +1,128 @@ +"use strict"; + +const test = require("node:test"); +const assert = require("node:assert/strict"); +const fs = require("node:fs"); +const os = require("node:os"); +const path = require("node:path"); + +const { + resolveUserDataRoot, + resolveUserDataRootFromInputs, +} = require("../getConfig"); +const { getPackagedUserDataRoot } = require("../shellforgePaths"); + +function createTempRoot(prefix) { + return fs.mkdtempSync(path.join(os.tmpdir(), prefix)); +} + +function writeConfigAtRoot(rootPath, configContent = "{}") { + const configDirectoryPath = path.join(rootPath, "config"); + fs.mkdirSync(configDirectoryPath, { recursive: true }); + fs.writeFileSync(path.join(configDirectoryPath, "config.json"), configContent, "utf8"); +} + +function withSavedEnv(envKeys, runTest) { + const savedValues = Object.fromEntries( + envKeys.map((envKey) => [envKey, process.env[envKey]]) + ); + + return async () => { + try { + await runTest(); + } finally { + envKeys.forEach((envKey) => { + const savedValue = savedValues[envKey]; + if (savedValue === undefined) { + delete process.env[envKey]; + } else { + process.env[envKey] = savedValue; + } + }); + } + }; +} + +test("resolveUserDataRootFromInputs prefers envUserDataRoot when set", () => { + const envRoot = createTempRoot("shellforge-env-root-"); + + assert.equal( + resolveUserDataRootFromInputs({ + envUserDataRoot: envRoot, + runtimeRoot: createTempRoot("shellforge-runtime-"), + appDataPath: createTempRoot("shellforge-appdata-"), + }), + path.resolve(envRoot) + ); +}); + +test("resolveUserDataRootFromInputs uses runtime root when config exists there", () => { + const runtimeRoot = createTempRoot("shellforge-runtime-config-"); + writeConfigAtRoot(runtimeRoot, '{"actionRunner":{}}'); + + assert.equal( + resolveUserDataRootFromInputs({ + envUserDataRoot: "", + runtimeRoot, + appDataPath: createTempRoot("shellforge-appdata-"), + }), + runtimeRoot + ); +}); + +test("resolveUserDataRootFromInputs uses packaged AppData root when only AppData config exists", () => { + const runtimeRoot = createTempRoot("shellforge-runtime-empty-"); + const appDataRoot = createTempRoot("shellforge-appdata-config-"); + const packagedUserDataRoot = getPackagedUserDataRoot(appDataRoot); + writeConfigAtRoot(packagedUserDataRoot, '{"actionRunner":{}}'); + + assert.equal( + resolveUserDataRootFromInputs({ + envUserDataRoot: "", + runtimeRoot, + appDataPath: appDataRoot, + }), + packagedUserDataRoot + ); +}); + +test("resolveUserDataRootFromInputs defaults to runtime root when no config exists", () => { + const runtimeRoot = createTempRoot("shellforge-runtime-default-"); + const appDataRoot = createTempRoot("shellforge-appdata-empty-"); + + assert.equal( + resolveUserDataRootFromInputs({ + envUserDataRoot: "", + runtimeRoot, + appDataPath: appDataRoot, + }), + runtimeRoot + ); +}); + +test("resolveUserDataRootFromInputs prefers runtime config over AppData when both exist", () => { + const runtimeRoot = createTempRoot("shellforge-runtime-both-"); + const appDataRoot = createTempRoot("shellforge-appdata-both-"); + const packagedUserDataRoot = getPackagedUserDataRoot(appDataRoot); + writeConfigAtRoot(runtimeRoot, '{"actionRunner":{"dev":{}}}'); + writeConfigAtRoot(packagedUserDataRoot, '{"actionRunner":{}}'); + + assert.equal( + resolveUserDataRootFromInputs({ + envUserDataRoot: "", + runtimeRoot, + appDataPath: appDataRoot, + }), + runtimeRoot + ); +}); + +test( + "resolveUserDataRoot wrapper prefers SHELLFORGE_USER_DATA when set", + withSavedEnv(["SHELLFORGE_USER_DATA", "APPDATA"], () => { + const envRoot = createTempRoot("shellforge-wrapper-env-"); + process.env.SHELLFORGE_USER_DATA = envRoot; + + assert.equal(resolveUserDataRoot(), path.resolve(envRoot)); + }) +);