From 3d83fdede38c6c5d65f3ba7b4fb33437f3504efb Mon Sep 17 00:00:00 2001 From: Joao Castro Date: Fri, 12 Jun 2026 08:30:12 -0300 Subject: [PATCH 1/2] fix: adjusting appdata resolve for cli commands --- CHANGELOG.md | 6 + commands/action-runner/action-runner.js | 9 +- package.json | 2 +- scripts/run-core-tests.mjs | 1 + utils/getConfig.js | 31 +++++- utils/shellforgePaths.js | 15 +++ utils/tests/getConfig.test.js | 142 ++++++++++++++++++++++++ 7 files changed, 194 insertions(+), 12 deletions(-) create mode 100644 utils/shellforgePaths.js create mode 100644 utils/tests/getConfig.test.js 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..53ee28a 100644 --- a/utils/getConfig.js +++ b/utils/getConfig.js @@ -1,13 +1,36 @@ const fs = require("node:fs"); const path = require("node:path"); +const { getPackagedUserDataRoot } = require("./shellforgePaths"); + +function getRuntimeRoot() { + return path.resolve(__dirname, ".."); +} + +function hasConfigAtRoot(rootPath) { + const configPath = path.join(rootPath, "config", "config.json"); + return fs.existsSync(configPath); +} function resolveUserDataRoot() { - const userDataRoot = process.env.SHELLFORGE_USER_DATA; - if (typeof userDataRoot === "string" && userDataRoot.trim().length > 0) { - return path.resolve(userDataRoot.trim()); + const envUserDataRoot = process.env.SHELLFORGE_USER_DATA; + if (typeof envUserDataRoot === "string" && envUserDataRoot.trim().length > 0) { + return path.resolve(envUserDataRoot.trim()); } - return path.resolve(__dirname, ".."); + const runtimeRoot = getRuntimeRoot(); + if (hasConfigAtRoot(runtimeRoot)) { + return runtimeRoot; + } + + const appDataPath = process.env.APPDATA; + if (typeof appDataPath === "string" && appDataPath.trim().length > 0) { + const packagedUserDataRoot = getPackagedUserDataRoot(appDataPath.trim()); + if (hasConfigAtRoot(packagedUserDataRoot)) { + return packagedUserDataRoot; + } + } + + return runtimeRoot; } function readConfigFile(configPath) { 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..469af97 --- /dev/null +++ b/utils/tests/getConfig.test.js @@ -0,0 +1,142 @@ +"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 } = require("../getConfig"); +const { getPackagedUserDataRoot } = require("../shellforgePaths"); + +const RUNTIME_ROOT = path.resolve(__dirname, "..", ".."); +const RUNTIME_CONFIG_PATH = path.join(RUNTIME_ROOT, "config", "config.json"); + +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]; + return; + } + + process.env[envKey] = savedValue; + }); + } + }; +} + +function withRuntimeConfigBackup(runTest) { + const hadRuntimeConfig = fs.existsSync(RUNTIME_CONFIG_PATH); + const runtimeConfigBackup = hadRuntimeConfig + ? fs.readFileSync(RUNTIME_CONFIG_PATH, "utf8") + : null; + + return async () => { + try { + await runTest(); + } finally { + if (hadRuntimeConfig && runtimeConfigBackup !== null) { + fs.writeFileSync(RUNTIME_CONFIG_PATH, runtimeConfigBackup, "utf8"); + return; + } + + if (fs.existsSync(RUNTIME_CONFIG_PATH)) { + fs.unlinkSync(RUNTIME_CONFIG_PATH); + } + } + }; +} + +test( + "resolveUserDataRoot prefers SHELLFORGE_USER_DATA when set", + withSavedEnv(["SHELLFORGE_USER_DATA", "APPDATA"], () => { + const envRoot = createTempRoot("shellforge-env-root-"); + process.env.SHELLFORGE_USER_DATA = envRoot; + + assert.equal(resolveUserDataRoot(), path.resolve(envRoot)); + }) +); + +test( + "resolveUserDataRoot uses runtime root when config exists there", + withSavedEnv(["SHELLFORGE_USER_DATA", "APPDATA"], () => { + delete process.env.SHELLFORGE_USER_DATA; + + assert.ok(fs.existsSync(RUNTIME_CONFIG_PATH)); + assert.equal(resolveUserDataRoot(), RUNTIME_ROOT); + }) +); + +test( + "resolveUserDataRoot uses packaged AppData root when only AppData config exists", + withSavedEnv(["SHELLFORGE_USER_DATA", "APPDATA"], async () => { + delete process.env.SHELLFORGE_USER_DATA; + + const appDataRoot = createTempRoot("shellforge-appdata-"); + const packagedUserDataRoot = getPackagedUserDataRoot(appDataRoot); + writeConfigAtRoot(packagedUserDataRoot, '{"actionRunner":{}}'); + process.env.APPDATA = appDataRoot; + + await withRuntimeConfigBackup(async () => { + if (fs.existsSync(RUNTIME_CONFIG_PATH)) { + fs.unlinkSync(RUNTIME_CONFIG_PATH); + } + + assert.equal(resolveUserDataRoot(), packagedUserDataRoot); + })(); + }) +); + +test( + "resolveUserDataRoot defaults to runtime root when no config exists", + withSavedEnv(["SHELLFORGE_USER_DATA", "APPDATA"], async () => { + delete process.env.SHELLFORGE_USER_DATA; + + const appDataRoot = createTempRoot("shellforge-empty-appdata-"); + process.env.APPDATA = appDataRoot; + + await withRuntimeConfigBackup(async () => { + if (fs.existsSync(RUNTIME_CONFIG_PATH)) { + fs.unlinkSync(RUNTIME_CONFIG_PATH); + } + + assert.equal(resolveUserDataRoot(), RUNTIME_ROOT); + })(); + }) +); + +test( + "resolveUserDataRoot prefers runtime config over AppData when both exist", + withSavedEnv(["SHELLFORGE_USER_DATA", "APPDATA"], async () => { + delete process.env.SHELLFORGE_USER_DATA; + + const appDataRoot = createTempRoot("shellforge-both-appdata-"); + const packagedUserDataRoot = getPackagedUserDataRoot(appDataRoot); + writeConfigAtRoot(packagedUserDataRoot, '{"actionRunner":{}}'); + process.env.APPDATA = appDataRoot; + + await withRuntimeConfigBackup(async () => { + writeConfigAtRoot(RUNTIME_ROOT, '{"actionRunner":{"dev":{}}}'); + assert.equal(resolveUserDataRoot(), RUNTIME_ROOT); + })(); + }) +); From 8582f1632afdf56b4d54805574dfe7d20e7fbb8b Mon Sep 17 00:00:00 2001 From: Joao Castro Date: Fri, 12 Jun 2026 08:45:13 -0300 Subject: [PATCH 2/2] fix: adjusting lint and action-runner-tests --- utils/getConfig.js | 25 +++-- utils/tests/getConfig.test.js | 174 ++++++++++++++++------------------ 2 files changed, 98 insertions(+), 101 deletions(-) diff --git a/utils/getConfig.js b/utils/getConfig.js index 53ee28a..e50478b 100644 --- a/utils/getConfig.js +++ b/utils/getConfig.js @@ -11,18 +11,20 @@ function hasConfigAtRoot(rootPath) { return fs.existsSync(configPath); } -function resolveUserDataRoot() { - const envUserDataRoot = process.env.SHELLFORGE_USER_DATA; +function resolveUserDataRootFromInputs({ + envUserDataRoot, + runtimeRoot, + appDataPath, +}) { if (typeof envUserDataRoot === "string" && envUserDataRoot.trim().length > 0) { return path.resolve(envUserDataRoot.trim()); } - const runtimeRoot = getRuntimeRoot(); - if (hasConfigAtRoot(runtimeRoot)) { - return runtimeRoot; + const resolvedRuntimeRoot = path.resolve(runtimeRoot); + if (hasConfigAtRoot(resolvedRuntimeRoot)) { + return resolvedRuntimeRoot; } - const appDataPath = process.env.APPDATA; if (typeof appDataPath === "string" && appDataPath.trim().length > 0) { const packagedUserDataRoot = getPackagedUserDataRoot(appDataPath.trim()); if (hasConfigAtRoot(packagedUserDataRoot)) { @@ -30,7 +32,15 @@ function resolveUserDataRoot() { } } - return runtimeRoot; + return resolvedRuntimeRoot; +} + +function resolveUserDataRoot() { + return resolveUserDataRootFromInputs({ + envUserDataRoot: process.env.SHELLFORGE_USER_DATA, + runtimeRoot: getRuntimeRoot(), + appDataPath: process.env.APPDATA, + }); } function readConfigFile(configPath) { @@ -49,4 +59,5 @@ async function getConfigs() { module.exports = { getConfigs, resolveUserDataRoot, + resolveUserDataRootFromInputs, }; diff --git a/utils/tests/getConfig.test.js b/utils/tests/getConfig.test.js index 469af97..66a0362 100644 --- a/utils/tests/getConfig.test.js +++ b/utils/tests/getConfig.test.js @@ -6,12 +6,12 @@ const fs = require("node:fs"); const os = require("node:os"); const path = require("node:path"); -const { resolveUserDataRoot } = require("../getConfig"); +const { + resolveUserDataRoot, + resolveUserDataRootFromInputs, +} = require("../getConfig"); const { getPackagedUserDataRoot } = require("../shellforgePaths"); -const RUNTIME_ROOT = path.resolve(__dirname, "..", ".."); -const RUNTIME_CONFIG_PATH = path.join(RUNTIME_ROOT, "config", "config.json"); - function createTempRoot(prefix) { return fs.mkdtempSync(path.join(os.tmpdir(), prefix)); } @@ -35,108 +35,94 @@ function withSavedEnv(envKeys, runTest) { const savedValue = savedValues[envKey]; if (savedValue === undefined) { delete process.env[envKey]; - return; + } else { + process.env[envKey] = savedValue; } - - process.env[envKey] = savedValue; }); } }; } -function withRuntimeConfigBackup(runTest) { - const hadRuntimeConfig = fs.existsSync(RUNTIME_CONFIG_PATH); - const runtimeConfigBackup = hadRuntimeConfig - ? fs.readFileSync(RUNTIME_CONFIG_PATH, "utf8") - : null; +test("resolveUserDataRootFromInputs prefers envUserDataRoot when set", () => { + const envRoot = createTempRoot("shellforge-env-root-"); - return async () => { - try { - await runTest(); - } finally { - if (hadRuntimeConfig && runtimeConfigBackup !== null) { - fs.writeFileSync(RUNTIME_CONFIG_PATH, runtimeConfigBackup, "utf8"); - return; - } - - if (fs.existsSync(RUNTIME_CONFIG_PATH)) { - fs.unlinkSync(RUNTIME_CONFIG_PATH); - } - } - }; -} + 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 prefers SHELLFORGE_USER_DATA when set", + "resolveUserDataRoot wrapper prefers SHELLFORGE_USER_DATA when set", withSavedEnv(["SHELLFORGE_USER_DATA", "APPDATA"], () => { - const envRoot = createTempRoot("shellforge-env-root-"); + const envRoot = createTempRoot("shellforge-wrapper-env-"); process.env.SHELLFORGE_USER_DATA = envRoot; assert.equal(resolveUserDataRoot(), path.resolve(envRoot)); }) ); - -test( - "resolveUserDataRoot uses runtime root when config exists there", - withSavedEnv(["SHELLFORGE_USER_DATA", "APPDATA"], () => { - delete process.env.SHELLFORGE_USER_DATA; - - assert.ok(fs.existsSync(RUNTIME_CONFIG_PATH)); - assert.equal(resolveUserDataRoot(), RUNTIME_ROOT); - }) -); - -test( - "resolveUserDataRoot uses packaged AppData root when only AppData config exists", - withSavedEnv(["SHELLFORGE_USER_DATA", "APPDATA"], async () => { - delete process.env.SHELLFORGE_USER_DATA; - - const appDataRoot = createTempRoot("shellforge-appdata-"); - const packagedUserDataRoot = getPackagedUserDataRoot(appDataRoot); - writeConfigAtRoot(packagedUserDataRoot, '{"actionRunner":{}}'); - process.env.APPDATA = appDataRoot; - - await withRuntimeConfigBackup(async () => { - if (fs.existsSync(RUNTIME_CONFIG_PATH)) { - fs.unlinkSync(RUNTIME_CONFIG_PATH); - } - - assert.equal(resolveUserDataRoot(), packagedUserDataRoot); - })(); - }) -); - -test( - "resolveUserDataRoot defaults to runtime root when no config exists", - withSavedEnv(["SHELLFORGE_USER_DATA", "APPDATA"], async () => { - delete process.env.SHELLFORGE_USER_DATA; - - const appDataRoot = createTempRoot("shellforge-empty-appdata-"); - process.env.APPDATA = appDataRoot; - - await withRuntimeConfigBackup(async () => { - if (fs.existsSync(RUNTIME_CONFIG_PATH)) { - fs.unlinkSync(RUNTIME_CONFIG_PATH); - } - - assert.equal(resolveUserDataRoot(), RUNTIME_ROOT); - })(); - }) -); - -test( - "resolveUserDataRoot prefers runtime config over AppData when both exist", - withSavedEnv(["SHELLFORGE_USER_DATA", "APPDATA"], async () => { - delete process.env.SHELLFORGE_USER_DATA; - - const appDataRoot = createTempRoot("shellforge-both-appdata-"); - const packagedUserDataRoot = getPackagedUserDataRoot(appDataRoot); - writeConfigAtRoot(packagedUserDataRoot, '{"actionRunner":{}}'); - process.env.APPDATA = appDataRoot; - - await withRuntimeConfigBackup(async () => { - writeConfigAtRoot(RUNTIME_ROOT, '{"actionRunner":{"dev":{}}}'); - assert.equal(resolveUserDataRoot(), RUNTIME_ROOT); - })(); - }) -);