Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 2 additions & 7 deletions commands/action-runner/action-runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -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);
Expand Down Expand Up @@ -120,7 +115,7 @@ function validateBrowserProfileName(browserProfile, actionName) {
function getBrowserLaunchOverrides(
actionConfig,
actionName,
projectRoot = USER_DATA_ROOT
projectRoot = resolveUserDataRoot()
) {
if (!actionConfig || typeof actionConfig !== "object") {
return {};
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
1 change: 1 addition & 0 deletions scripts/run-core-tests.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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)), "..");
Expand Down
44 changes: 39 additions & 5 deletions utils/getConfig.js
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -26,4 +59,5 @@ async function getConfigs() {
module.exports = {
getConfigs,
resolveUserDataRoot,
resolveUserDataRootFromInputs,
};
15 changes: 15 additions & 0 deletions utils/shellforgePaths.js
Original file line number Diff line number Diff line change
@@ -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,
};
128 changes: 128 additions & 0 deletions utils/tests/getConfig.test.js
Original file line number Diff line number Diff line change
@@ -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));
})
);
Loading