* refactor(shell-projection): remove deprecated wrappers + finalize ADRs (Phase 4, #3468) Final phase of the shell-command-projection expansion. Removes the legacy core.cjs wrappers (`atomicWriteFileSync`, `safeReadFile`, `normalizeMd`) now that every call site lives behind the seam, plus three Phase-3 stragglers (`graphify.cjs`, `template.cjs`, dead import in `profile-pipeline.cjs`). Documentation: - ADR-0009: addendum noting Phase 1–4 scope expansion (subprocess + file I/O ownership), supersession of "does not execute" constraint, and resolution of open Q4. - ADR-0010: status changed to Superseded by ADR-0009 with explanation. - CONTEXT.md "Shell Command Projection Module" entry already current from Phase 1 — no edit needed. Tests: - `tests/atomic-write.test.cjs` deleted — wrapper it tested is gone; `atomic-write-coverage.test.cjs` (Phase 3) covers platformWriteSync. - `tests/core.test.cjs::safeReadFile` + `::normalizeMd` describes deleted — wrappers are gone. - `tests/concurrency-safety.test.cjs` normalizeMd suite (behavioral / perf / snapshot) repointed via 2-line shim at the seam's `normalizeContent` — full regression coverage preserved. Test result: 9059/9041/18 — exact pre-Phase-4 baseline. All 18 failures are pre-existing path-with-spaces local-env issues. Closes #3468 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor(shell-projection): migrate remaining raw fs.writeFileSync sites (Phase 4, #3468) Sweeps the 7 raw fs.writeFileSync call sites that bypassed the seam through Phase 3, folding them into platformWriteSync. Net -14 lines: deletes the local writeFileAtomicSync helper in installer-migrations.cjs and collapses surface.cjs's manual tmp+rename into a single seam call. Sites migrated: - drift.cjs (1) — frontmatter write - learnings.cjs (1) — learning record JSON write - install-profiles.cjs (1) — profile marker write (collapsed redundant mkdir) - gsd2-import.cjs (1) — imported file write (collapsed redundant mkdir) - surface.cjs (1) — surface state write (replaced manual tmp+rename block) - installer-migrations.cjs (3) — journal init/finalize + rewrite-json action; deleted private writeFileAtomicSync helper and its three call sites Two sites intentionally retained outside the seam: - planning-workspace.cjs:241 — workspace lock (wx-flag atomic-create; previously excluded by Phase 3) - installer-migrations.cjs:220 — install migration lock (fd write into wx-opened handle) - writeInstallState (installer-migrations.cjs) — strict atomic contract for install state; the seam's fallback-to-direct-write on rename failure would silently violate the invariant that install state must never be left half-written. Inline tmp+rename with rethrow keeps the original guarantee. Tests: 9059 / 9041 / 18 — exactly the pre-Phase-4 baseline; 18 failures are the pre-existing path-with-spaces local-env issues, identical files as before. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(installer-migrations): use strict atomic write for rollback install-state restore The rollback path was restoring INSTALL_STATE via platformWriteSync, which falls back to a direct write on rename failure and would silently violate the half-written invariant that the install-state contract guarantees elsewhere. Extracts the strict tmp+rename logic from writeInstallState into a shared atomicWriteInstallState(configDir, content) helper and routes both writeInstallState and rollbackAppliedMigrationResult through it. Preserves the existing null-handling (rmSync when previousInstallStateBytes === null) and existing failure-collection (failures.push on caught errors). Byte-faithful restore: previousInstallStateBytes is written as-is (no JSON parse round-trip), preserving the exact prior file contents on restore. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -278,14 +278,6 @@ function error(message, reason = ERROR_REASON.UNKNOWN) {
|
||||
|
||||
// ─── File & Config utilities ──────────────────────────────────────────────────
|
||||
|
||||
function safeReadFile(filePath) {
|
||||
try {
|
||||
return fs.readFileSync(filePath, 'utf-8');
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Canonical config defaults. Single source of truth — imported by config.cjs and verify.cjs.
|
||||
*/
|
||||
@@ -610,118 +602,6 @@ function isGitIgnored(cwd, targetPath) {
|
||||
return ignored;
|
||||
}
|
||||
|
||||
// ─── Markdown normalization ─────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Normalize markdown to fix common markdownlint violations.
|
||||
* Applied at write points so GSD-generated .planning/ files are IDE-friendly.
|
||||
*
|
||||
* Rules enforced:
|
||||
* MD022 — Blank lines around headings
|
||||
* MD031 — Blank lines around fenced code blocks
|
||||
* MD032 — Blank lines around lists
|
||||
* MD012 — No multiple consecutive blank lines (collapsed to 2 max)
|
||||
* MD047 — Files end with a single newline
|
||||
*/
|
||||
function normalizeMd(content) {
|
||||
if (!content || typeof content !== 'string') return content;
|
||||
|
||||
// Normalize line endings to LF for consistent processing
|
||||
let text = content.replace(/\r\n/g, '\n');
|
||||
|
||||
const lines = text.split('\n');
|
||||
const result = [];
|
||||
|
||||
// Pre-compute fence state in a single O(n) pass instead of O(n^2) per-line scanning
|
||||
const fenceRegex = /^```/;
|
||||
const insideFence = new Array(lines.length);
|
||||
let fenceOpen = false;
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
if (fenceRegex.test(lines[i].trimEnd())) {
|
||||
if (fenceOpen) {
|
||||
// This is a closing fence — mark as NOT inside (it's the boundary)
|
||||
insideFence[i] = false;
|
||||
fenceOpen = false;
|
||||
} else {
|
||||
// This is an opening fence
|
||||
insideFence[i] = false;
|
||||
fenceOpen = true;
|
||||
}
|
||||
} else {
|
||||
insideFence[i] = fenceOpen;
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
const prev = i > 0 ? lines[i - 1] : '';
|
||||
const prevTrimmed = prev.trimEnd();
|
||||
const trimmed = line.trimEnd();
|
||||
const isFenceLine = fenceRegex.test(trimmed);
|
||||
|
||||
// MD022: Blank line before headings (skip first line and frontmatter delimiters)
|
||||
if (/^#{1,6}\s/.test(trimmed) && i > 0 && prevTrimmed !== '' && prevTrimmed !== '---') {
|
||||
result.push('');
|
||||
}
|
||||
|
||||
// MD031: Blank line before fenced code blocks (opening fences only)
|
||||
if (isFenceLine && i > 0 && prevTrimmed !== '' && !insideFence[i] && (i === 0 || !insideFence[i - 1] || isFenceLine)) {
|
||||
// Only add blank before opening fences (not closing ones)
|
||||
if (i === 0 || !insideFence[i - 1]) {
|
||||
result.push('');
|
||||
}
|
||||
}
|
||||
|
||||
// MD032: Blank line before lists (- item, * item, N. item, - [ ] item)
|
||||
if (/^(\s*[-*+]\s|\s*\d+\.\s)/.test(line) && i > 0 &&
|
||||
prevTrimmed !== '' && !/^(\s*[-*+]\s|\s*\d+\.\s)/.test(prev) &&
|
||||
prevTrimmed !== '---') {
|
||||
result.push('');
|
||||
}
|
||||
|
||||
result.push(line);
|
||||
|
||||
// MD022: Blank line after headings
|
||||
if (/^#{1,6}\s/.test(trimmed) && i < lines.length - 1) {
|
||||
const next = lines[i + 1];
|
||||
if (next !== undefined && next.trimEnd() !== '') {
|
||||
result.push('');
|
||||
}
|
||||
}
|
||||
|
||||
// MD031: Blank line after closing fenced code blocks
|
||||
if (/^```\s*$/.test(trimmed) && i > 0 && insideFence[i - 1] && i < lines.length - 1) {
|
||||
const next = lines[i + 1];
|
||||
if (next !== undefined && next.trimEnd() !== '') {
|
||||
result.push('');
|
||||
}
|
||||
}
|
||||
|
||||
// MD032: Blank line after last list item in a block
|
||||
if (/^(\s*[-*+]\s|\s*\d+\.\s)/.test(line) && i < lines.length - 1) {
|
||||
const next = lines[i + 1];
|
||||
if (next !== undefined && next.trimEnd() !== '' &&
|
||||
!/^(\s*[-*+]\s|\s*\d+\.\s)/.test(next) &&
|
||||
!/^\s/.test(next)) {
|
||||
// Only add blank line if next line is not a continuation/indented line
|
||||
result.push('');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
text = result.join('\n');
|
||||
|
||||
// MD012: Collapse 3+ consecutive blank lines to 2
|
||||
text = text.replace(/\n{3,}/g, '\n\n');
|
||||
|
||||
// MD047: Ensure file ends with exactly one newline
|
||||
text = text.replace(/\n*$/, '\n');
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
// Default timeout for worktree-related git subprocess calls (matches worktree-safety.cjs).
|
||||
// Prevents `git worktree list --porcelain` and similar calls from blocking the parent
|
||||
// ─── Common path helpers ──────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
@@ -1884,38 +1764,6 @@ function readSubdirectories(dirPath, sort = false) {
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Atomic file writes ───────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Write a file atomically using write-to-temp-then-rename.
|
||||
*
|
||||
* On POSIX systems, `fs.renameSync` is atomic when the source and destination
|
||||
* are on the same filesystem. This prevents a process killed mid-write from
|
||||
* leaving a truncated file that is unparseable on next read.
|
||||
*
|
||||
* The temp file is placed alongside the target so it is guaranteed to be on
|
||||
* the same filesystem (required for rename atomicity). The PID is embedded in
|
||||
* the temp file name so concurrent writers use distinct paths.
|
||||
*
|
||||
* If `renameSync` fails (e.g. cross-device move), the function falls back to a
|
||||
* direct `writeFileSync` so callers always get a best-effort write.
|
||||
*
|
||||
* @param {string} filePath Absolute path to write.
|
||||
* @param {string|Buffer} content File content.
|
||||
* @param {string} [encoding='utf-8'] Encoding passed to writeFileSync.
|
||||
*/
|
||||
function atomicWriteFileSync(filePath, content, encoding = 'utf-8') {
|
||||
const tmpPath = filePath + '.tmp.' + process.pid;
|
||||
try {
|
||||
fs.writeFileSync(tmpPath, content, encoding);
|
||||
fs.renameSync(tmpPath, filePath);
|
||||
} catch (renameErr) {
|
||||
// Clean up the temp file if rename failed, then fall back to direct write.
|
||||
try { fs.unlinkSync(tmpPath); } catch { /* already gone or never created */ }
|
||||
fs.writeFileSync(filePath, content, encoding);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a Date as a fuzzy relative time string (e.g. "5 minutes ago").
|
||||
* @param {Date} date
|
||||
@@ -1948,10 +1796,8 @@ module.exports = {
|
||||
ERROR_REASON,
|
||||
setJsonErrorMode,
|
||||
getJsonErrorMode,
|
||||
safeReadFile,
|
||||
loadConfig,
|
||||
isGitIgnored,
|
||||
normalizeMd,
|
||||
escapeRegex,
|
||||
normalizePhaseName,
|
||||
comparePhaseNum,
|
||||
@@ -1999,7 +1845,6 @@ module.exports = {
|
||||
readSubdirectories,
|
||||
getAgentsDir,
|
||||
checkAgentsInstalled,
|
||||
atomicWriteFileSync,
|
||||
timeAgo,
|
||||
pruneOrphanedWorktrees,
|
||||
inspectWorktreeHealth,
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('node:fs');
|
||||
const { platformWriteSync } = require('./shell-command-projection.cjs');
|
||||
|
||||
// ─── Constants ───────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -360,7 +361,7 @@ function writeMappedCommit(filePath, commitSha, isoDate) {
|
||||
const { data, body } = parseFrontmatter(content);
|
||||
data.last_mapped_commit = commitSha;
|
||||
if (isoDate) data.last_mapped_at = isoDate;
|
||||
fs.writeFileSync(filePath, serializeFrontmatter(data, body));
|
||||
platformWriteSync(filePath, serializeFrontmatter(data, body));
|
||||
}
|
||||
|
||||
// ─── Exports ─────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execTool, execGit } = require('./shell-command-projection.cjs');
|
||||
const { atomicWriteFileSync } = require('./core.cjs');
|
||||
const { execTool, execGit, platformWriteSync } = require('./shell-command-projection.cjs');
|
||||
|
||||
// ─── Config Gate ─────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -523,7 +522,7 @@ function graphifyBuild(cwd) {
|
||||
/**
|
||||
* Write a diff snapshot after successful build (D-06).
|
||||
* Reads graph.json from .planning/graphs/ and writes .last-build-snapshot.json
|
||||
* using atomicWriteFileSync for crash safety.
|
||||
* using platformWriteSync for crash safety.
|
||||
*
|
||||
* @param {string} cwd - Working directory
|
||||
* @returns {object}
|
||||
@@ -541,7 +540,7 @@ function writeSnapshot(cwd) {
|
||||
};
|
||||
|
||||
const snapshotPath = path.join(cwd, '.planning', 'graphs', '.last-build-snapshot.json');
|
||||
atomicWriteFileSync(snapshotPath, JSON.stringify(snapshot, null, 2));
|
||||
platformWriteSync(snapshotPath, JSON.stringify(snapshot, null, 2));
|
||||
return {
|
||||
saved: true,
|
||||
timestamp: snapshot.timestamp,
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { platformWriteSync } = require('./shell-command-projection.cjs');
|
||||
|
||||
// ─── Utilities ──────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -434,8 +435,7 @@ function buildPreview(gsd2Data, artifacts) {
|
||||
function writePlanningDir(artifacts, planningRoot) {
|
||||
for (const [rel, content] of artifacts) {
|
||||
const absPath = path.join(planningRoot, rel);
|
||||
fs.mkdirSync(path.dirname(absPath), { recursive: true });
|
||||
fs.writeFileSync(absPath, content, 'utf8');
|
||||
platformWriteSync(absPath, content);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
const { platformWriteSync } = require('./shell-command-projection.cjs');
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Profile definitions
|
||||
@@ -404,8 +405,7 @@ function readActiveProfile(runtimeConfigDir) {
|
||||
* @param {string} profileName e.g. 'core', 'standard', 'full'
|
||||
*/
|
||||
function writeActiveProfile(runtimeConfigDir, profileName) {
|
||||
fs.mkdirSync(runtimeConfigDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(runtimeConfigDir, PROFILE_MARKER_NAME), profileName + '\n', 'utf8');
|
||||
platformWriteSync(path.join(runtimeConfigDir, PROFILE_MARKER_NAME), profileName + '\n');
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -7,6 +7,7 @@ const {
|
||||
validateInstallerMigrationActions,
|
||||
validateInstallerMigrationRecord,
|
||||
} = require('./installer-migration-authoring.cjs');
|
||||
const { platformWriteSync } = require('./shell-command-projection.cjs');
|
||||
|
||||
const MANIFEST_NAME = 'gsd-file-manifest.json';
|
||||
const INSTALL_STATE_NAME = 'gsd-install-state.json';
|
||||
@@ -71,9 +72,24 @@ function readInstallState(configDir) {
|
||||
};
|
||||
}
|
||||
|
||||
function writeInstallState(configDir, state) {
|
||||
// Strict atomic write for the install state: must never be left half-written.
|
||||
// Bypasses the seam because platformWriteSync falls back to a direct write on
|
||||
// rename failure, which would silently violate this invariant.
|
||||
function atomicWriteInstallState(configDir, content) {
|
||||
fs.mkdirSync(configDir, { recursive: true });
|
||||
writeFileAtomicSync(path.join(configDir, INSTALL_STATE_NAME), JSON.stringify(state, null, 2) + '\n');
|
||||
const filePath = path.join(configDir, INSTALL_STATE_NAME);
|
||||
const tmpPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
|
||||
try {
|
||||
fs.writeFileSync(tmpPath, content, 'utf8');
|
||||
fs.renameSync(tmpPath, filePath);
|
||||
} catch (error) {
|
||||
try { fs.rmSync(tmpPath, { force: true }); } catch { /* best-effort */ }
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function writeInstallState(configDir, state) {
|
||||
atomicWriteInstallState(configDir, JSON.stringify(state, null, 2) + '\n');
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -263,20 +279,6 @@ function isStructurallyEmpty(value) {
|
||||
return typeof value === 'object' && Object.keys(value).length === 0;
|
||||
}
|
||||
|
||||
function writeFileAtomicSync(filePath, content) {
|
||||
const tmpPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
|
||||
try {
|
||||
fs.writeFileSync(tmpPath, content, 'utf8');
|
||||
fs.renameSync(tmpPath, filePath);
|
||||
} catch (error) {
|
||||
try {
|
||||
fs.rmSync(tmpPath, { force: true });
|
||||
} catch {
|
||||
// best-effort cleanup only; preserve the original write failure
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function journalAction(action, status, extras = {}) {
|
||||
const { value, ...safeAction } = action;
|
||||
@@ -424,8 +426,7 @@ function rollbackAppliedMigrationResult({ configDir, journal, journalPath, rollb
|
||||
if (previousInstallStateBytes === null) {
|
||||
fs.rmSync(path.join(configDir, INSTALL_STATE_NAME), { force: true });
|
||||
} else {
|
||||
fs.mkdirSync(configDir, { recursive: true });
|
||||
writeFileAtomicSync(path.join(configDir, INSTALL_STATE_NAME), previousInstallStateBytes);
|
||||
atomicWriteInstallState(configDir, previousInstallStateBytes);
|
||||
}
|
||||
} catch (error) {
|
||||
failures.push({ relPath: INSTALL_STATE_NAME, error: error.message });
|
||||
@@ -476,12 +477,12 @@ function applyInstallerMigrationPlan({ configDir, plan, now = () => new Date().t
|
||||
const rollback = [];
|
||||
const installStatePath = path.join(configDir, INSTALL_STATE_NAME);
|
||||
const previousInstallStateBytes = fs.existsSync(installStatePath)
|
||||
? fs.readFileSync(installStatePath)
|
||||
? fs.readFileSync(installStatePath, 'utf8')
|
||||
: null;
|
||||
|
||||
try {
|
||||
fs.mkdirSync(path.dirname(journalPath), { recursive: true });
|
||||
fs.writeFileSync(journalPath, JSON.stringify(journal, null, 2) + '\n', 'utf8');
|
||||
platformWriteSync(journalPath, JSON.stringify(journal, null, 2) + '\n');
|
||||
|
||||
for (const action of plan.actions) {
|
||||
if (
|
||||
@@ -517,7 +518,7 @@ function applyInstallerMigrationPlan({ configDir, plan, now = () => new Date().t
|
||||
rollbackRelPath: path.posix.join(rollbackRootRelPath, normalized),
|
||||
}));
|
||||
} else {
|
||||
writeFileAtomicSync(fullPath, JSON.stringify(action.value, null, 2) + '\n');
|
||||
platformWriteSync(fullPath, JSON.stringify(action.value, null, 2) + '\n');
|
||||
journal.actions.push(journalAction(action, 'rewritten', {
|
||||
rollbackRelPath: path.posix.join(rollbackRootRelPath, normalized),
|
||||
}));
|
||||
@@ -542,7 +543,7 @@ function applyInstallerMigrationPlan({ configDir, plan, now = () => new Date().t
|
||||
fs.rmSync(fullPath, { force: true });
|
||||
}
|
||||
|
||||
fs.writeFileSync(journalPath, JSON.stringify(journal, null, 2) + '\n', 'utf8');
|
||||
platformWriteSync(journalPath, JSON.stringify(journal, null, 2) + '\n');
|
||||
|
||||
const state = readInstallState(configDir);
|
||||
const applied = appliedMigrationIds(state);
|
||||
|
||||
@@ -18,6 +18,7 @@ const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
const os = require('os');
|
||||
const { output, error: coreError } = require('./core.cjs');
|
||||
const { platformWriteSync } = require('./shell-command-projection.cjs');
|
||||
|
||||
// ─── Constants ───────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -125,7 +126,7 @@ function learningsWrite(entry, opts) {
|
||||
content_hash: hash,
|
||||
};
|
||||
|
||||
fs.writeFileSync(path.join(dir, `${id}.json`), JSON.stringify(record, null, 2), 'utf-8');
|
||||
platformWriteSync(path.join(dir, `${id}.json`), JSON.stringify(record, null, 2));
|
||||
return { id, created: true, content_hash: hash };
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
const readline = require('readline');
|
||||
const { output, error, safeReadFile, reapStaleTempFiles } = require('./core.cjs');
|
||||
const { output, error, reapStaleTempFiles } = require('./core.cjs');
|
||||
|
||||
// ─── Session I/O Helpers ──────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
const { platformWriteSync } = require('./shell-command-projection.cjs');
|
||||
|
||||
const {
|
||||
readActiveProfile,
|
||||
@@ -74,17 +75,13 @@ function readSurface(runtimeConfigDir) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the surface state atomically (write to tmp then rename).
|
||||
* Write the surface state atomically via the platform seam (mkdir + tmp+rename).
|
||||
*
|
||||
* @param {string} runtimeConfigDir
|
||||
* @param {SurfaceState} surfaceState
|
||||
*/
|
||||
function writeSurface(runtimeConfigDir, surfaceState) {
|
||||
fs.mkdirSync(runtimeConfigDir, { recursive: true });
|
||||
const finalPath = path.join(runtimeConfigDir, SURFACE_FILE_NAME);
|
||||
const tmpPath = finalPath + '.tmp.' + process.pid;
|
||||
fs.writeFileSync(tmpPath, JSON.stringify(surfaceState, null, 2) + '\n', 'utf8');
|
||||
fs.renameSync(tmpPath, finalPath);
|
||||
platformWriteSync(path.join(runtimeConfigDir, SURFACE_FILE_NAME), JSON.stringify(surfaceState, null, 2) + '\n');
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { normalizePhaseName, findPhaseInternal, generateSlugInternal, normalizeMd, toPosixPath, output, error } = require('./core.cjs');
|
||||
const { normalizePhaseName, findPhaseInternal, generateSlugInternal, toPosixPath, output, error } = require('./core.cjs');
|
||||
const { platformWriteSync } = require('./shell-command-projection.cjs');
|
||||
const { planningDir } = require('./planning-workspace.cjs');
|
||||
const { reconstructFrontmatter } = require('./frontmatter.cjs');
|
||||
|
||||
@@ -219,7 +220,7 @@ function cmdTemplateFill(cwd, templateType, options, raw) {
|
||||
return;
|
||||
}
|
||||
|
||||
fs.writeFileSync(outPath, normalizeMd(fullContent), 'utf-8');
|
||||
platformWriteSync(outPath, fullContent);
|
||||
const relPath = toPosixPath(path.relative(cwd, outPath));
|
||||
output({ created: true, path: relPath, template: templateType }, raw, relPath);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user