@savvy-web/github-action-builder 2.1.1 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,282 +0,0 @@
1
- import { BundleFailed, CleanError, WriteError } from "../errors.js";
2
- import { BuildService } from "./build.js";
3
- import { ConfigService } from "./config.js";
4
- import { buildNativeDynamicImportRules } from "./native-dynamic-imports.js";
5
- import { Effect, Layer, Result } from "effect";
6
- import { existsSync, mkdirSync, readFileSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs";
7
- import { basename, resolve } from "node:path";
8
- import { fileURLToPath } from "node:url";
9
- import { createRsbuild } from "@rsbuild/core";
10
-
11
- //#region src/services/build-live.ts
12
- /* v8 ignore start - build service requires actual bundling for integration testing */
13
- /**
14
- * BuildService Layer implementation.
15
- *
16
- */
17
- /**
18
- * Source of the stub module that replaces packages listed in `build.ignore`.
19
- * It is bundled in place of the real module and throws if ever loaded.
20
- */
21
- const IGNORE_STUB_SOURCE = `throw new Error("A module excluded via the build 'ignore' option was loaded at runtime.");\n`;
22
- /**
23
- * Self-referencing specifier for the `webpackIgnore`-injecting loader
24
- * shipped from `public/loaders/webpack-ignore-dynamic-imports.cjs` (see
25
- * `package.json` `exports`). Resolved through the package's own `exports`
26
- * map via `import.meta.resolve`, which stays correct whether this module is
27
- * running from `src` (the map points at `./public/loaders/...`) or from a
28
- * built `dist` (the map points at the flattened `./loaders/...`, since the
29
- * `public/` copy step drops the `public/` prefix both on disk and in the
30
- * built manifest) — no relative-path assumption needed either way.
31
- */
32
- const WEBPACK_IGNORE_LOADER_SPECIFIER = "@savvy-web/github-action-builder/loaders/webpack-ignore-dynamic-imports.cjs";
33
- /**
34
- * Resolve the absolute on-disk path to the `webpackIgnore`-injecting loader.
35
- */
36
- function resolveWebpackIgnoreLoaderPath() {
37
- return fileURLToPath(import.meta.resolve(WEBPACK_IGNORE_LOADER_SPECIFIER));
38
- }
39
- /**
40
- * Format bytes as a human-readable string.
41
- */
42
- function formatBytes(bytes) {
43
- if (bytes < 1024) return `${bytes} B`;
44
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
45
- return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
46
- }
47
- /**
48
- * Format build result for terminal output.
49
- */
50
- /* v8 ignore start - formatting function tested via integration */
51
- function formatBuildResult(result) {
52
- const lines = [];
53
- if (result.success) {
54
- lines.push("Build Summary:");
55
- for (const entry of result.entries) if (entry.success && entry.stats) {
56
- const { entry: name, size, duration, outputPath } = entry.stats;
57
- lines.push(` ✓ ${name}: ${formatBytes(size)} (${duration}ms) → ${outputPath}`);
58
- }
59
- lines.push(`\nTotal time: ${result.duration}ms`);
60
- } else {
61
- lines.push("Build Failed:");
62
- for (const entry of result.entries) if (!entry.success) lines.push(` ✗ ${entry.error}`);
63
- }
64
- return lines.join("\n");
65
- }
66
- /* v8 ignore stop */
67
- /**
68
- * Clean output directory.
69
- */
70
- function cleanDirectory(dir) {
71
- return Effect.try({
72
- try: () => {
73
- if (existsSync(dir)) rmSync(dir, {
74
- recursive: true,
75
- force: true
76
- });
77
- },
78
- /* v8 ignore next 5 - error branch requires fs permission failures */
79
- catch: (error) => new CleanError({
80
- directory: dir,
81
- cause: error
82
- })
83
- });
84
- }
85
- /**
86
- * Write file with directory creation.
87
- */
88
- function writeFile(path, content) {
89
- return Effect.try({
90
- try: () => {
91
- mkdirSync(resolve(path, ".."), { recursive: true });
92
- writeFileSync(path, content, "utf8");
93
- },
94
- /* v8 ignore next 5 - error branch requires fs permission failures */
95
- catch: (error) => new WriteError({
96
- path,
97
- cause: error
98
- })
99
- });
100
- }
101
- /**
102
- * Fold an extracted `*.LICENSE.txt` sidecar back into its bundle.
103
- *
104
- * `legalComments: "linked"` is the only mode whose extraction actually sees
105
- * bundled license banners — the "inline" mode's SWC comment-preservation path
106
- * never receives them and silently drops attribution (verified against
107
- * rsbuild 2.1.8). A committed action still must not carry sidecar files
108
- * (issue #94), so the sidecar's verbatim comment blocks replace the
109
- * `LICENSE:` reference banner in the bundle and the sidecar is deleted —
110
- * attribution inline, no extra dist file.
111
- */
112
- function inlineLicenseSidecar(outputPath) {
113
- return Effect.try({
114
- try: () => {
115
- const sidecarPath = `${outputPath}.LICENSE.txt`;
116
- if (!existsSync(sidecarPath)) return;
117
- const licenses = readFileSync(sidecarPath, "utf8").trim();
118
- const bundle = readFileSync(outputPath, "utf8");
119
- const reference = `/*! LICENSE: ${basename(sidecarPath)} */`;
120
- const afterReference = bundle.startsWith(reference) && bundle.charAt(reference.length) === "\n" ? reference.length + 1 : bundle.startsWith(reference) ? reference.length : 0;
121
- writeFileSync(outputPath, `${licenses}\n${bundle.slice(afterReference)}`, "utf8");
122
- unlinkSync(sidecarPath);
123
- },
124
- /* v8 ignore next 5 - error branch requires fs permission failures */
125
- catch: (error) => new WriteError({
126
- path: outputPath,
127
- cause: error
128
- })
129
- });
130
- }
131
- /**
132
- * Bundle a single entry with rsbuild.
133
- */
134
- /* v8 ignore start - bundling requires actual rsbuild execution */
135
- function bundleEntry(entry, config, cwd) {
136
- return Effect.gen(function* () {
137
- const startTime = Date.now();
138
- const outputDir = resolve(cwd, "dist");
139
- const externalsSet = new Set(config.build.externals);
140
- const ignoreSet = new Set(config.build.ignore);
141
- const ignoreAlias = {};
142
- // webpackIgnore-injecting loader below leaves those calls as native
143
- const nativeDynamicImportRules = config.build.nativeDynamicImports.length > 0 ? buildNativeDynamicImportRules(config.build.nativeDynamicImports, resolveWebpackIgnoreLoaderPath()) : [];
144
- if (config.build.ignore.length > 0) {
145
- const stubPath = resolve(cwd, "node_modules", ".cache", "github-action-builder", "ignore-stub.mjs");
146
- yield* writeFile(stubPath, IGNORE_STUB_SOURCE);
147
- for (const moduleName of config.build.ignore) ignoreAlias[`${moduleName}$`] = stubPath;
148
- }
149
- const rsbuild = yield* Effect.tryPromise({
150
- try: () => createRsbuild({ rsbuildConfig: {
151
- mode: "production",
152
- source: { entry: { [entry.type]: entry.path } },
153
- resolve: { alias: ignoreAlias },
154
- output: {
155
- target: "node",
156
- module: true,
157
- distPath: { root: outputDir },
158
- filename: { js: "[name].js" },
159
- externals: (data) => {
160
- const request = data.request;
161
- if (!request) return false;
162
- if (request.startsWith("node:")) return `node-commonjs ${request}`;
163
- if (externalsSet.has(request) && !ignoreSet.has(request)) return request;
164
- return false;
165
- },
166
- cleanDistPath: false,
167
- legalComments: "linked",
168
- minify: config.build.minify,
169
- sourceMap: config.build.sourceMap ? { js: "source-map" } : false
170
- },
171
- performance: { chunkSplit: { strategy: "all-in-one" } },
172
- tools: { rspack: {
173
- node: {
174
- __dirname: "node-module",
175
- __filename: "node-module"
176
- },
177
- module: {
178
- parser: { javascript: { importMeta: false } },
179
- // webpackIgnore-injecting loader (empty when the option is unset).
180
- rules: nativeDynamicImportRules
181
- },
182
- output: { asyncChunks: false }
183
- } }
184
- } }),
185
- catch: (error) => new BundleFailed({
186
- entry: entry.path,
187
- cause: error
188
- })
189
- });
190
- const buildResult = yield* Effect.tryPromise({
191
- try: () => rsbuild.build(),
192
- catch: (error) => new BundleFailed({
193
- entry: entry.path,
194
- cause: error
195
- })
196
- });
197
- yield* Effect.tryPromise({
198
- try: () => buildResult.close(),
199
- catch: (error) => new BundleFailed({
200
- entry: entry.path,
201
- cause: /* @__PURE__ */ new Error(`rsbuild close() failed: ${error}`)
202
- })
203
- });
204
- const outputPath = resolve(outputDir, `${entry.type}.js`);
205
- yield* inlineLicenseSidecar(outputPath);
206
- const size = yield* Effect.try({
207
- try: () => statSync(outputPath).size,
208
- catch: (error) => new BundleFailed({
209
- entry: entry.path,
210
- cause: error
211
- })
212
- });
213
- const duration = Date.now() - startTime;
214
- return {
215
- success: true,
216
- stats: {
217
- entry: entry.type,
218
- size,
219
- duration,
220
- outputPath: entry.output
221
- }
222
- };
223
- });
224
- }
225
- /* v8 ignore stop */
226
- /**
227
- * Live implementation of BuildService.
228
- *
229
- * @remarks
230
- * Uses rsbuild for bundling.
231
- */
232
- const BuildServiceLive = Layer.effect(BuildService, Effect.gen(function* () {
233
- const configService = yield* ConfigService;
234
- return {
235
- /* v8 ignore start - build execution requires actual rsbuild bundling */
236
- build: (config, options = {}) => Effect.gen(function* () {
237
- const cwd = options.cwd ?? process.cwd();
238
- const shouldClean = options.clean ?? true;
239
- const startTime = Date.now();
240
- const entriesConfig = { main: config.entries.main };
241
- if (config.entries.pre) entriesConfig.pre = config.entries.pre;
242
- if (config.entries.post) entriesConfig.post = config.entries.post;
243
- if (config.entries.workers) entriesConfig.workers = config.entries.workers;
244
- const entriesResult = yield* configService.detectEntries(cwd, entriesConfig);
245
- if (shouldClean) yield* cleanDirectory(resolve(cwd, "dist"));
246
- const entryResults = [];
247
- for (const entry of entriesResult.entries) {
248
- const result = yield* Effect.result(bundleEntry(entry, config, cwd));
249
- if (Result.isFailure(result)) {
250
- const err = result.failure;
251
- entryResults.push({
252
- success: false,
253
- error: err.cause instanceof Error ? err.cause.message : String(err.cause)
254
- });
255
- } else entryResults.push(result.success);
256
- }
257
- yield* writeFile(resolve(cwd, "dist/package.json"), "{ \"type\": \"module\" }");
258
- const duration = Date.now() - startTime;
259
- const success = entryResults.every((r) => r.success);
260
- if (!success) return {
261
- success,
262
- entries: entryResults,
263
- duration,
264
- error: "One or more entries failed to build"
265
- };
266
- return {
267
- success,
268
- entries: entryResults,
269
- duration
270
- };
271
- }),
272
- /* v8 ignore stop */
273
- bundle: (entry, config) => bundleEntry(entry, config, process.cwd()),
274
- clean: (outputDir) => cleanDirectory(outputDir),
275
- formatResult: formatBuildResult,
276
- formatBytes
277
- };
278
- }));
279
- /* v8 ignore stop */
280
-
281
- //#endregion
282
- export { BuildServiceLive };
@@ -1,138 +0,0 @@
1
- import { ConfigInvalid, ConfigLoadFailed, ConfigNotFound, MainEntryMissing, WorkerEntryInvalidName, WorkerEntryMissing } from "../errors.js";
2
- import { defineConfig } from "../schemas/config.js";
3
- import { ConfigService } from "./config.js";
4
- import { Effect, Layer } from "effect";
5
- import { existsSync } from "node:fs";
6
- import { resolve } from "node:path";
7
- import { createJiti } from "jiti";
8
-
9
- //#region src/services/config-live.ts
10
- /**
11
- * ConfigService Layer implementation.
12
- *
13
- */
14
- const CONFIG_FILENAMES = [
15
- "action.config.ts",
16
- "action.config.js",
17
- "action.config.mjs"
18
- ];
19
- const DEFAULT_ENTRIES = {
20
- main: "src/main.ts",
21
- pre: "src/pre.ts",
22
- post: "src/post.ts"
23
- };
24
- /** Lifecycle bundle names a worker entry must not reuse — they own `dist/main.js` etc. */
25
- const RESERVED_ENTRY_NAMES = /* @__PURE__ */ new Set([
26
- "main",
27
- "pre",
28
- "post"
29
- ]);
30
- /**
31
- * Find config file in the given directory.
32
- */
33
- function findConfigFile(cwd) {
34
- for (const filename of CONFIG_FILENAMES) {
35
- const configPath = resolve(cwd, filename);
36
- if (existsSync(configPath)) return configPath;
37
- }
38
- }
39
- /**
40
- * Detect a single optional entry.
41
- */
42
- function detectOptionalEntry(cwd, type, explicitPath) {
43
- const defaultPath = DEFAULT_ENTRIES[type];
44
- const absolutePath = resolve(cwd, explicitPath ?? defaultPath);
45
- if (existsSync(absolutePath)) return {
46
- type,
47
- path: absolutePath,
48
- output: `dist/${type}.js`
49
- };
50
- }
51
- /**
52
- * Live implementation of ConfigService.
53
- */
54
- const ConfigServiceLive = Layer.succeed(ConfigService, {
55
- load: (options = {}) => Effect.gen(function* () {
56
- const cwd = options.cwd ?? process.cwd();
57
- const configPath = options.configPath ?? findConfigFile(cwd);
58
- if (!configPath) return {
59
- config: defineConfig({}),
60
- usingDefaults: true
61
- };
62
- /* v8 ignore start - requires explicit configPath to non-existent file */
63
- if (!existsSync(configPath)) return yield* Effect.fail(new ConfigNotFound({
64
- path: configPath,
65
- message: "Specified config file does not exist"
66
- }));
67
- /* v8 ignore stop */
68
- const absolutePath = resolve(cwd, configPath);
69
- /* v8 ignore stop */
70
- const configInput = (yield* Effect.tryPromise({
71
- try: async () => {
72
- if (absolutePath.endsWith(".ts")) return createJiti(absolutePath, { interopDefault: true }).import(absolutePath);
73
- return import(absolutePath);
74
- },
75
- catch: (error) => new ConfigLoadFailed({
76
- path: configPath,
77
- cause: error
78
- })
79
- })).default;
80
- /* v8 ignore start - requires config file with non-object default export */
81
- if (!configInput || typeof configInput !== "object") return yield* Effect.fail(new ConfigInvalid({
82
- path: configPath,
83
- errors: ["Config file must export a default configuration object"]
84
- }));
85
- return {
86
- config: defineConfig(configInput),
87
- configPath,
88
- usingDefaults: false
89
- };
90
- }),
91
- resolve: (input = {}) => Effect.succeed(defineConfig(input)),
92
- detectEntries: (cwd, entries) => Effect.gen(function* () {
93
- const detected = [];
94
- const mainPath = entries?.main ?? DEFAULT_ENTRIES.main;
95
- const absoluteMainPath = resolve(cwd, mainPath);
96
- if (!existsSync(absoluteMainPath)) return yield* Effect.fail(new MainEntryMissing({
97
- expectedPath: mainPath,
98
- cwd
99
- }));
100
- detected.push({
101
- type: "main",
102
- path: absoluteMainPath,
103
- output: "dist/main.js"
104
- });
105
- const preEntry = detectOptionalEntry(cwd, "pre", entries?.pre);
106
- if (preEntry) detected.push(preEntry);
107
- const postEntry = detectOptionalEntry(cwd, "post", entries?.post);
108
- if (postEntry) detected.push(postEntry);
109
- for (const [name, workerPath] of Object.entries(entries?.workers ?? {})) {
110
- if (RESERVED_ENTRY_NAMES.has(name)) return yield* Effect.fail(new WorkerEntryInvalidName({
111
- workerName: name,
112
- reason: `"${name}" is a reserved lifecycle bundle name (main/pre/post)`
113
- }));
114
- if (name.length === 0 || name.includes("/") || name.includes("\\") || name.includes("..")) return yield* Effect.fail(new WorkerEntryInvalidName({
115
- workerName: name,
116
- reason: "worker names must be non-empty and free of path separators"
117
- }));
118
- const absoluteWorkerPath = resolve(cwd, workerPath);
119
- if (!existsSync(absoluteWorkerPath)) return yield* Effect.fail(new WorkerEntryMissing({
120
- workerName: name,
121
- expectedPath: workerPath,
122
- cwd
123
- }));
124
- detected.push({
125
- type: name,
126
- path: absoluteWorkerPath,
127
- output: `dist/${name}.js`
128
- });
129
- }
130
- return {
131
- success: true,
132
- entries: detected
133
- };
134
- })
135
- });
136
-
137
- //#endregion
138
- export { ConfigServiceLive };
@@ -1,211 +0,0 @@
1
- import { ActionYmlPathError, PersistLocalError } from "../errors.js";
2
- import { PersistLocalService } from "./persist-local.js";
3
- import { Effect, Layer } from "effect";
4
- import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
5
- import { dirname, join, relative, resolve } from "node:path";
6
- import { createHash } from "node:crypto";
7
- import { Yaml } from "@effected/yaml";
8
-
9
- //#region src/services/persist-local-live.ts
10
- /**
11
- * PersistLocalService Layer implementation.
12
- *
13
- */
14
- /**
15
- * Compute SHA-256 hash of a file's contents.
16
- */
17
- function fileHash(filePath) {
18
- const content = readFileSync(filePath);
19
- return createHash("sha256").update(content).digest("hex");
20
- }
21
- /**
22
- * Sync a single file from src to dest using hash comparison.
23
- * Returns true if the file was copied, false if skipped.
24
- */
25
- function syncFile(src, dest) {
26
- if (existsSync(dest)) {
27
- if (fileHash(src) === fileHash(dest)) return false;
28
- }
29
- mkdirSync(dirname(dest), { recursive: true });
30
- copyFileSync(src, dest);
31
- return true;
32
- }
33
- /**
34
- * Recursively collect all file paths relative to a base directory.
35
- */
36
- function walkDirectory(dir, base = dir) {
37
- const files = [];
38
- if (!existsSync(dir)) return files;
39
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
40
- const fullPath = join(dir, entry.name);
41
- if (entry.isDirectory()) files.push(...walkDirectory(fullPath, base));
42
- else files.push(relative(base, fullPath));
43
- }
44
- return files;
45
- }
46
- /**
47
- * Smart-sync a directory: copy changed files, remove stale dest files.
48
- */
49
- function syncDirectory(srcDir, destDir) {
50
- const stats = {
51
- copied: 0,
52
- skipped: 0
53
- };
54
- const srcFiles = walkDirectory(srcDir);
55
- for (const relPath of srcFiles) if (syncFile(join(srcDir, relPath), join(destDir, relPath))) stats.copied++;
56
- else stats.skipped++;
57
- const srcFileSet = new Set(srcFiles);
58
- const destFiles = walkDirectory(destDir);
59
- for (const relPath of destFiles) if (!srcFileSet.has(relPath)) {
60
- rmSync(join(destDir, relPath), { force: true });
61
- let parent = dirname(join(destDir, relPath));
62
- while (parent !== destDir && existsSync(parent)) if (readdirSync(parent).length === 0) {
63
- rmSync(parent, { recursive: true });
64
- parent = dirname(parent);
65
- } else break;
66
- }
67
- return stats;
68
- }
69
- /**
70
- * Validate that action.yml runs paths resolve correctly relative to the destination.
71
- */
72
- function validateActionYmlPaths(actionYmlPath, destDir) {
73
- return Effect.gen(function* () {
74
- if (!existsSync(actionYmlPath)) return;
75
- const content = readFileSync(actionYmlPath, "utf8");
76
- const parsed = yield* Yaml.parse(content).pipe(Effect.catch(() => Effect.succeed(null)));
77
- if (!parsed?.runs) return;
78
- for (const entryType of [
79
- "main",
80
- "pre",
81
- "post"
82
- ]) {
83
- const specifiedPath = parsed.runs[entryType];
84
- if (!specifiedPath) continue;
85
- const expectedPath = resolve(destDir, specifiedPath);
86
- if (!existsSync(expectedPath)) return yield* Effect.fail(new ActionYmlPathError({
87
- entryType,
88
- specifiedPath,
89
- expectedPath
90
- }));
91
- }
92
- });
93
- }
94
- const ACTRC_CONTENT = `--container-architecture linux/amd64
95
- -W .github/workflows/act-test.yml
96
- `;
97
- const ACT_WORKFLOW_CONTENT = `name: Local Test
98
- on:
99
- workflow_dispatch:
100
-
101
- jobs:
102
- test:
103
- runs-on: ubuntu-latest
104
- steps:
105
- - uses: actions/checkout@v7
106
- - uses: ./.github/actions/local
107
- `;
108
- function formatPersistResult(result) {
109
- const lines = [];
110
- if (result.success) {
111
- lines.push("Persist Local Summary:");
112
- lines.push(` Output: ${result.outputPath}`);
113
- lines.push(` Files copied: ${result.filesCopied}`);
114
- lines.push(` Files skipped (unchanged): ${result.filesSkipped}`);
115
- if (result.actTemplateGenerated) lines.push(" Act template files generated");
116
- } else lines.push(`Persist Local Failed: ${result.error}`);
117
- return lines.join("\n");
118
- }
119
- /**
120
- * Live implementation of PersistLocalService.
121
- */
122
- const PersistLocalServiceLive = Layer.succeed(PersistLocalService, {
123
- persist: (config, options = {}) => Effect.gen(function* () {
124
- const cwd = options.cwd ?? process.cwd();
125
- const outputPath = resolve(cwd, config.persistLocal.path);
126
- if (!config.persistLocal.enabled) return {
127
- success: true,
128
- filesCopied: 0,
129
- filesSkipped: 0,
130
- actTemplateGenerated: false,
131
- outputPath
132
- };
133
- yield* Effect.try({
134
- try: () => mkdirSync(outputPath, { recursive: true }),
135
- /* v8 ignore next 5 - error branch requires fs permission failures */
136
- catch: (error) => new PersistLocalError({
137
- path: outputPath,
138
- cause: error
139
- })
140
- });
141
- let totalCopied = 0;
142
- let totalSkipped = 0;
143
- const actionYmlSrc = resolve(cwd, "action.yml");
144
- const actionYmlDest = resolve(outputPath, "action.yml");
145
- if (existsSync(actionYmlSrc)) if (yield* Effect.try({
146
- try: () => syncFile(actionYmlSrc, actionYmlDest),
147
- /* v8 ignore next 5 - error branch requires fs permission failures */
148
- catch: (error) => new PersistLocalError({
149
- path: actionYmlSrc,
150
- cause: error
151
- })
152
- })) totalCopied++;
153
- else totalSkipped++;
154
- else if (existsSync(actionYmlDest)) rmSync(actionYmlDest, { force: true });
155
- const distSrc = resolve(cwd, "dist");
156
- if (existsSync(distSrc) && statSync(distSrc).isDirectory()) {
157
- const distStats = yield* Effect.try({
158
- try: () => syncDirectory(distSrc, resolve(outputPath, "dist")),
159
- /* v8 ignore next 5 - error branch requires fs permission failures */
160
- catch: (error) => new PersistLocalError({
161
- path: distSrc,
162
- cause: error
163
- })
164
- });
165
- totalCopied += distStats.copied;
166
- totalSkipped += distStats.skipped;
167
- }
168
- yield* validateActionYmlPaths(resolve(outputPath, "action.yml"), outputPath);
169
- let actTemplateGenerated = false;
170
- if (config.persistLocal.actTemplate) {
171
- const actrcPath = resolve(cwd, ".actrc");
172
- const actWorkflowPath = resolve(cwd, ".github/workflows/act-test.yml");
173
- if (!existsSync(actrcPath)) {
174
- yield* Effect.try({
175
- try: () => writeFileSync(actrcPath, ACTRC_CONTENT, "utf8"),
176
- /* v8 ignore next 5 - error branch requires fs permission failures */
177
- catch: (error) => new PersistLocalError({
178
- path: actrcPath,
179
- cause: error
180
- })
181
- });
182
- actTemplateGenerated = true;
183
- }
184
- if (!existsSync(actWorkflowPath)) {
185
- yield* Effect.try({
186
- try: () => {
187
- mkdirSync(dirname(actWorkflowPath), { recursive: true });
188
- writeFileSync(actWorkflowPath, ACT_WORKFLOW_CONTENT, "utf8");
189
- },
190
- /* v8 ignore next 5 - error branch requires fs permission failures */
191
- catch: (error) => new PersistLocalError({
192
- path: actWorkflowPath,
193
- cause: error
194
- })
195
- });
196
- actTemplateGenerated = true;
197
- }
198
- }
199
- return {
200
- success: true,
201
- filesCopied: totalCopied,
202
- filesSkipped: totalSkipped,
203
- actTemplateGenerated,
204
- outputPath
205
- };
206
- }),
207
- formatResult: formatPersistResult
208
- });
209
-
210
- //#endregion
211
- export { PersistLocalServiceLive };