@whamp/pi-pstack 0.7.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.
- package/LICENSE +21 -0
- package/README.md +68 -0
- package/agents/comment-sicko.md +34 -0
- package/agents/poteto-agent.md +13 -0
- package/extensions/pstack/ask-user-question.ts +137 -0
- package/extensions/pstack/config.ts +55 -0
- package/extensions/pstack/index.ts +330 -0
- package/extensions/pstack/pstack-config-status.ts +72 -0
- package/extensions/pstack/pstack-role-config-store.ts +287 -0
- package/extensions/pstack/pstack-role-config.ts +496 -0
- package/extensions/pstack/pstack-role-prompt.ts +33 -0
- package/extensions/pstack/pstack-roles.ts +252 -0
- package/extensions/pstack/pstack-setup-plan.ts +223 -0
- package/extensions/pstack/skill-strip.ts +62 -0
- package/package.json +60 -0
- package/skills/architect/SKILL.md +83 -0
- package/skills/architect/references/design-red-flags.md +33 -0
- package/skills/architect/references/rationale-template.md +35 -0
- package/skills/architect/references/runner-prompt.md +20 -0
- package/skills/arena/SKILL.md +71 -0
- package/skills/automate-me/SKILL.md +104 -0
- package/skills/blast-radius/SKILL.md +50 -0
- package/skills/bro/SKILL.md +7 -0
- package/skills/create-verification-skill/SKILL.md +44 -0
- package/skills/create-verification-skill/references/feature-map-example/README.md +47 -0
- package/skills/create-verification-skill/references/feature-map-example/create-note.md +39 -0
- package/skills/create-verification-skill/references/feature-map-example/search.md +45 -0
- package/skills/deslop/SKILL.md +23 -0
- package/skills/figure-it-out/SKILL.md +53 -0
- package/skills/how/SKILL.md +52 -0
- package/skills/how/references/explainer-prompt.md +55 -0
- package/skills/how/references/explorer-prompt.md +52 -0
- package/skills/interrogate/SKILL.md +111 -0
- package/skills/interrogate/references/code-quality-review.md +47 -0
- package/skills/interrogate/references/lead-judgment.md +58 -0
- package/skills/interrogate/references/reviewer-prompt.md +72 -0
- package/skills/interrogate/references/rubric.md +77 -0
- package/skills/maintain-verification-skill/SKILL.md +39 -0
- package/skills/no-comments/SKILL.md +24 -0
- package/skills/poteto-mode/SKILL.md +147 -0
- package/skills/poteto-mode/playbooks/authoring-a-skill.md +12 -0
- package/skills/poteto-mode/playbooks/autonomous-run.md +13 -0
- package/skills/poteto-mode/playbooks/autopilot-full.md +13 -0
- package/skills/poteto-mode/playbooks/autopilot-stack.md +16 -0
- package/skills/poteto-mode/playbooks/babysit.md +27 -0
- package/skills/poteto-mode/playbooks/bug-fix.md +17 -0
- package/skills/poteto-mode/playbooks/eval.md +25 -0
- package/skills/poteto-mode/playbooks/feature.md +21 -0
- package/skills/poteto-mode/playbooks/hillclimb.md +21 -0
- package/skills/poteto-mode/playbooks/investigation.md +14 -0
- package/skills/poteto-mode/playbooks/multi-phase-plan.md +156 -0
- package/skills/poteto-mode/playbooks/opening-a-pr.md +33 -0
- package/skills/poteto-mode/playbooks/orchestrate.md +199 -0
- package/skills/poteto-mode/playbooks/pause-safely.md +10 -0
- package/skills/poteto-mode/playbooks/perf-issue.md +24 -0
- package/skills/poteto-mode/playbooks/prototype.md +14 -0
- package/skills/poteto-mode/playbooks/refactoring.md +16 -0
- package/skills/poteto-mode/playbooks/runtime-forensics.md +11 -0
- package/skills/poteto-mode/playbooks/session-pickup.md +11 -0
- package/skills/poteto-mode/playbooks/shipping.md +17 -0
- package/skills/poteto-mode/playbooks/trace-forensics.md +14 -0
- package/skills/poteto-mode/playbooks/visual-parity.md +11 -0
- package/skills/poteto-mode/playbooks/worktree-cleanup.md +14 -0
- package/skills/poteto-mode/references/bugbot-triage.md +142 -0
- package/skills/poteto-mode/scripts/bootstrap.ts +62 -0
- package/skills/poteto-mode/scripts/bun.lock +67 -0
- package/skills/poteto-mode/scripts/check-plan.mjs +186 -0
- package/skills/poteto-mode/scripts/check-plan.test.mjs +25 -0
- package/skills/poteto-mode/scripts/orch/orch.test.ts +634 -0
- package/skills/poteto-mode/scripts/orch/orch.ts +578 -0
- package/skills/poteto-mode/scripts/orch/store.ts +1607 -0
- package/skills/poteto-mode/scripts/package.json +16 -0
- package/skills/poteto-mode/scripts/watch-pr/cli.test.ts +224 -0
- package/skills/poteto-mode/scripts/watch-pr/cli.ts +223 -0
- package/skills/poteto-mode/scripts/watch-pr/fakes.test-helper.ts +118 -0
- package/skills/poteto-mode/scripts/watch-pr/github.test.ts +306 -0
- package/skills/poteto-mode/scripts/watch-pr/github.ts +699 -0
- package/skills/poteto-mode/scripts/watch-pr/policy.test.ts +420 -0
- package/skills/poteto-mode/scripts/watch-pr/policy.ts +832 -0
- package/skills/poteto-mode/scripts/watch-pr/render.ts +169 -0
- package/skills/poteto-mode/scripts/watch-pr/tsconfig.json +13 -0
- package/skills/poteto-mode/scripts/watch-pr/types.compile.ts +93 -0
- package/skills/poteto-mode/scripts/watch-pr/types.ts +401 -0
- package/skills/poteto-mode/scripts/watch-pr/watch-pr +6 -0
- package/skills/poteto-mode/scripts/worktree-audit.sh +85 -0
- package/skills/principle-attack-the-premise/SKILL.md +23 -0
- package/skills/principle-boundary-discipline/SKILL.md +34 -0
- package/skills/principle-build-the-lever/SKILL.md +23 -0
- package/skills/principle-encode-lessons-in-structure/SKILL.md +31 -0
- package/skills/principle-exhaust-the-design-space/SKILL.md +21 -0
- package/skills/principle-experience-first/SKILL.md +19 -0
- package/skills/principle-fix-root-causes/SKILL.md +23 -0
- package/skills/principle-foundational-thinking/SKILL.md +21 -0
- package/skills/principle-guard-the-context-window/SKILL.md +17 -0
- package/skills/principle-laziness-protocol/SKILL.md +18 -0
- package/skills/principle-make-operations-idempotent/SKILL.md +24 -0
- package/skills/principle-migrate-callers-then-delete-legacy-apis/SKILL.md +22 -0
- package/skills/principle-minimize-reader-load/SKILL.md +23 -0
- package/skills/principle-model-the-domain/SKILL.md +26 -0
- package/skills/principle-never-block-on-the-human/SKILL.md +22 -0
- package/skills/principle-outcome-oriented-execution/SKILL.md +22 -0
- package/skills/principle-prove-it-works/SKILL.md +33 -0
- package/skills/principle-redesign-from-first-principles/SKILL.md +16 -0
- package/skills/principle-separate-before-serializing-shared-state/SKILL.md +16 -0
- package/skills/principle-sequence-verifiable-units/SKILL.md +22 -0
- package/skills/principle-subtract-before-you-add/SKILL.md +21 -0
- package/skills/principle-test-behavior-not-implementation/SKILL.md +25 -0
- package/skills/principle-type-system-discipline/SKILL.md +31 -0
- package/skills/recall/SKILL.md +35 -0
- package/skills/reflect/SKILL.md +73 -0
- package/skills/reflect/references/divergent-reviewer.md +43 -0
- package/skills/reflect/references/judgment-reviewer.md +42 -0
- package/skills/reflect/references/synthesizer.md +56 -0
- package/skills/reflect/references/tooling-reviewer.md +55 -0
- package/skills/setup-pstack/SKILL.md +38 -0
- package/skills/setup-pstack/references/MODEL-ROLES.md +35 -0
- package/skills/show-me-your-work/SKILL.md +82 -0
- package/skills/show-me-your-work/references/decision-log-template.tsv +1 -0
- package/skills/show-me-your-work/scripts/log.sh +40 -0
- package/skills/swarm/SKILL.md +46 -0
- package/skills/tdd/SKILL.md +44 -0
- package/skills/teach/SKILL.md +21 -0
- package/skills/technical-writing/SKILL.md +127 -0
- package/skills/typescript-best-practices/SKILL.md +29 -0
- package/skills/typescript-best-practices/references/patterns.md +313 -0
- package/skills/unslop/SKILL.md +67 -0
- package/skills/why/SKILL.md +154 -0
- package/skills/why/references/epistemics.md +144 -0
- package/skills/why/references/investigator-prompt.md +103 -0
- package/skills/why/references/source-playbook.md +17 -0
- package/skills/why/references/sources/code-archaeology.md +88 -0
- package/skills/why/references/sources/databricks.md +70 -0
- package/skills/why/references/sources/datadog.md +99 -0
- package/skills/why/references/sources/incident-postmortem.md +15 -0
- package/skills/why/references/sources/linear.md +48 -0
- package/skills/why/references/sources/notion.md +55 -0
- package/skills/why/references/sources/sentry.md +100 -0
- package/skills/why/references/sources/slack.md +54 -0
- package/skills/why/references/synthesizer-prompt.md +135 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { PstackConfigDiagnostic, PstackConfigReadResult } from "./pstack-role-config.ts";
|
|
2
|
+
|
|
3
|
+
/** How /setup-pstack may persist the completed wizard. */
|
|
4
|
+
export type PstackSetupSaveKind = "create" | "atomic-v2" | "backup-legacy" | "confirm-replace";
|
|
5
|
+
|
|
6
|
+
function attentionDiagnostics(
|
|
7
|
+
diagnostics: readonly PstackConfigDiagnostic[],
|
|
8
|
+
): PstackConfigDiagnostic[] {
|
|
9
|
+
return diagnostics.filter(
|
|
10
|
+
(diagnostic) => diagnostic.severity === "warning" || diagnostic.severity === "error",
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function formatSourceState(source: PstackConfigReadResult["source"]): string {
|
|
15
|
+
switch (source) {
|
|
16
|
+
case "v2":
|
|
17
|
+
return "Source: v2.";
|
|
18
|
+
case "v1":
|
|
19
|
+
return "Source: v1. Migrated in memory. Run /setup-pstack to save v2.";
|
|
20
|
+
case "markdown":
|
|
21
|
+
return "Source: markdown. Migrated in memory. Run /setup-pstack to save v2.";
|
|
22
|
+
case "invalid":
|
|
23
|
+
return "Source: invalid. Using inherited defaults.";
|
|
24
|
+
case "missing":
|
|
25
|
+
return "Source: missing.";
|
|
26
|
+
default: {
|
|
27
|
+
const unhandledSource: never = source;
|
|
28
|
+
return unhandledSource;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function formatDiagnosticLine(diagnostic: PstackConfigDiagnostic): string {
|
|
34
|
+
const role = diagnostic.role ? ` [${diagnostic.role}]` : "";
|
|
35
|
+
return `${diagnostic.code}${role}: ${diagnostic.message}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Human status for /pstack status. Skills, source, and warning/error counts. */
|
|
39
|
+
export function formatPstackStatus(result: PstackConfigReadResult): string {
|
|
40
|
+
const skills = result.config.skillsEnabled
|
|
41
|
+
? "pstack skills on."
|
|
42
|
+
: "pstack skills off. Hidden from the model; /skill:<name> still works.";
|
|
43
|
+
const noisy = attentionDiagnostics(result.diagnostics);
|
|
44
|
+
const warnings = noisy.filter((diagnostic) => diagnostic.severity === "warning").length;
|
|
45
|
+
const errors = noisy.filter((diagnostic) => diagnostic.severity === "error").length;
|
|
46
|
+
const lines = [
|
|
47
|
+
skills,
|
|
48
|
+
formatSourceState(result.source),
|
|
49
|
+
`Warnings: ${warnings}. Errors: ${errors}.`,
|
|
50
|
+
...noisy.map(formatDiagnosticLine),
|
|
51
|
+
];
|
|
52
|
+
return lines.join("\n");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** One TUI warning when config has warnings or errors. Silent for missing and info-only. */
|
|
56
|
+
export function pstackSessionStartWarning(result: PstackConfigReadResult): string | undefined {
|
|
57
|
+
if (attentionDiagnostics(result.diagnostics).length === 0) return undefined;
|
|
58
|
+
return "pstack config needs attention. Run /pstack status.";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** True when /pstack on|off may write. Only missing or clean v2. */
|
|
62
|
+
export function canPersistPstackSkillsToggle(result: PstackConfigReadResult): boolean {
|
|
63
|
+
return result.source === "missing" || (result.source === "v2" && result.diagnostics.length === 0);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Persistence path for a completed /setup-pstack run. */
|
|
67
|
+
export function pstackSetupSaveKind(result: PstackConfigReadResult): PstackSetupSaveKind {
|
|
68
|
+
if (result.source === "missing") return "create";
|
|
69
|
+
if (result.source === "v1" || result.source === "markdown") return "backup-legacy";
|
|
70
|
+
if (result.source === "v2" && result.diagnostics.length === 0) return "atomic-v2";
|
|
71
|
+
return "confirm-replace";
|
|
72
|
+
}
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
linkSync,
|
|
4
|
+
lstatSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
renameSync,
|
|
8
|
+
unlinkSync,
|
|
9
|
+
writeFileSync,
|
|
10
|
+
} from "node:fs";
|
|
11
|
+
import { basename, dirname, join } from "node:path";
|
|
12
|
+
import { configPath, legacyMarkdownPath } from "./config.ts";
|
|
13
|
+
import {
|
|
14
|
+
decodePstackConfigText,
|
|
15
|
+
decodePstackLegacyMarkdownText,
|
|
16
|
+
defaultPstackRoleConfig,
|
|
17
|
+
pstackConfigDiagnostic,
|
|
18
|
+
type PstackConfigDiagnosticCode,
|
|
19
|
+
type PstackConfigReadResult,
|
|
20
|
+
type PstackConfigWriteResult,
|
|
21
|
+
} from "./pstack-role-config.ts";
|
|
22
|
+
import { PSTACK_ROLES, isPstackRoleName, type PstackRoleConfig } from "./pstack-roles.ts";
|
|
23
|
+
|
|
24
|
+
const MAX_CONFIG_BYTES = 100_000;
|
|
25
|
+
|
|
26
|
+
function isErrnoException(error: unknown): error is NodeJS.ErrnoException {
|
|
27
|
+
return typeof error === "object" && error !== null && "code" in error;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function cleanupTemporaryFile(path: string): string {
|
|
31
|
+
try {
|
|
32
|
+
if (existsSync(path)) unlinkSync(path);
|
|
33
|
+
return "";
|
|
34
|
+
} catch {
|
|
35
|
+
return ` Temporary file ${path} could not be removed.`;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function invalidReadResult(
|
|
40
|
+
code: PstackConfigDiagnosticCode,
|
|
41
|
+
message: string,
|
|
42
|
+
): PstackConfigReadResult {
|
|
43
|
+
return {
|
|
44
|
+
config: defaultPstackRoleConfig(),
|
|
45
|
+
source: "invalid",
|
|
46
|
+
diagnostics: [pstackConfigDiagnostic(code, "error", message)],
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function readFailure(path: string): PstackConfigReadResult {
|
|
51
|
+
return invalidReadResult(
|
|
52
|
+
"config-read-failed",
|
|
53
|
+
`Failed to read ${path}. Using default inherited roles.`,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function notRegularFile(path: string): PstackConfigReadResult {
|
|
58
|
+
return invalidReadResult(
|
|
59
|
+
"not-regular-file",
|
|
60
|
+
`${path} is not a regular file. Using default inherited roles.`,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function tooLarge(path: string): PstackConfigReadResult {
|
|
65
|
+
return invalidReadResult(
|
|
66
|
+
"config-too-large",
|
|
67
|
+
`${path} exceeds ${MAX_CONFIG_BYTES} bytes. Using default inherited roles.`,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function loadMarkdownOrMissing(jsonPath: string, markdownPath: string): PstackConfigReadResult {
|
|
72
|
+
let st: ReturnType<typeof lstatSync>;
|
|
73
|
+
try {
|
|
74
|
+
st = lstatSync(markdownPath);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
if (isErrnoException(error) && error.code === "ENOENT") {
|
|
77
|
+
return { config: defaultPstackRoleConfig(), source: "missing", diagnostics: [] };
|
|
78
|
+
}
|
|
79
|
+
return readFailure(markdownPath);
|
|
80
|
+
}
|
|
81
|
+
if (!st.isFile()) return notRegularFile(markdownPath);
|
|
82
|
+
if (st.size > MAX_CONFIG_BYTES) return tooLarge(markdownPath);
|
|
83
|
+
let text: string;
|
|
84
|
+
try {
|
|
85
|
+
text = readFileSync(markdownPath, "utf8");
|
|
86
|
+
} catch {
|
|
87
|
+
return readFailure(markdownPath);
|
|
88
|
+
}
|
|
89
|
+
if (text.length > MAX_CONFIG_BYTES) return tooLarge(markdownPath);
|
|
90
|
+
const migrated = decodePstackLegacyMarkdownText(text, markdownPath);
|
|
91
|
+
return { ...migrated, source: "markdown" };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function readPstackRoleConfigFile(path: string, markdownPath: string): PstackConfigReadResult {
|
|
95
|
+
let st: ReturnType<typeof lstatSync>;
|
|
96
|
+
try {
|
|
97
|
+
st = lstatSync(path);
|
|
98
|
+
} catch (error) {
|
|
99
|
+
if (isErrnoException(error) && error.code === "ENOENT")
|
|
100
|
+
return loadMarkdownOrMissing(path, markdownPath);
|
|
101
|
+
return readFailure(path);
|
|
102
|
+
}
|
|
103
|
+
if (!st.isFile()) return notRegularFile(path);
|
|
104
|
+
if (st.size > MAX_CONFIG_BYTES) return tooLarge(path);
|
|
105
|
+
let text: string;
|
|
106
|
+
try {
|
|
107
|
+
text = readFileSync(path, "utf8");
|
|
108
|
+
} catch {
|
|
109
|
+
return readFailure(path);
|
|
110
|
+
}
|
|
111
|
+
if (text.length > MAX_CONFIG_BYTES) return tooLarge(path);
|
|
112
|
+
return decodePstackConfigText(text, path);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Load pstack role config. Missing files are silent inherit-all. Never throws or rewrites on read. */
|
|
116
|
+
export function loadPstackRoleConfig(
|
|
117
|
+
path: string = configPath(),
|
|
118
|
+
markdownPath: string = legacyMarkdownPath(),
|
|
119
|
+
): PstackConfigReadResult {
|
|
120
|
+
try {
|
|
121
|
+
return readPstackRoleConfigFile(path, markdownPath);
|
|
122
|
+
} catch {
|
|
123
|
+
return readFailure(path);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function serializePstackRoleConfig(config: PstackRoleConfig): string {
|
|
128
|
+
const roles: Record<string, string | readonly string[]> = {};
|
|
129
|
+
for (const name of Object.keys(PSTACK_ROLES).filter(isPstackRoleName)) {
|
|
130
|
+
const value = config.roles[name];
|
|
131
|
+
if (value === undefined || value === "inherit-parent") continue;
|
|
132
|
+
roles[name] = value;
|
|
133
|
+
}
|
|
134
|
+
return `${JSON.stringify({ version: 2, roles, skillsEnabled: config.skillsEnabled }, null, 2)}\n`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function writeAtomicUtf8File(
|
|
138
|
+
path: string,
|
|
139
|
+
body: string,
|
|
140
|
+
): { ok: true } | { ok: false; message: string } {
|
|
141
|
+
const dir = dirname(path);
|
|
142
|
+
const tmp = join(dir, `.${basename(path)}.${process.pid}.${Date.now()}.tmp`);
|
|
143
|
+
try {
|
|
144
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
145
|
+
try {
|
|
146
|
+
const st = lstatSync(path);
|
|
147
|
+
if (!st.isFile()) return { ok: false, message: `${path} is not a regular file.` };
|
|
148
|
+
} catch (error) {
|
|
149
|
+
if (!(isErrnoException(error) && error.code === "ENOENT")) {
|
|
150
|
+
return { ok: false, message: `Failed to write ${path}.` };
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
writeFileSync(tmp, body, { encoding: "utf8", mode: 0o600 });
|
|
154
|
+
renameSync(tmp, path);
|
|
155
|
+
return { ok: true };
|
|
156
|
+
} catch {
|
|
157
|
+
return { ok: false, message: `Failed to write ${path}.${cleanupTemporaryFile(tmp)}` };
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function writeNewAtomicUtf8File(
|
|
162
|
+
path: string,
|
|
163
|
+
body: string,
|
|
164
|
+
): { ok: true } | { ok: false; message: string } {
|
|
165
|
+
const dir = dirname(path);
|
|
166
|
+
const tmp = join(dir, `.${basename(path)}.${process.pid}.${Date.now()}.tmp`);
|
|
167
|
+
try {
|
|
168
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
169
|
+
writeFileSync(tmp, body, { encoding: "utf8", mode: 0o600, flag: "wx" });
|
|
170
|
+
linkSync(tmp, path);
|
|
171
|
+
unlinkSync(tmp);
|
|
172
|
+
return { ok: true };
|
|
173
|
+
} catch {
|
|
174
|
+
return {
|
|
175
|
+
ok: false,
|
|
176
|
+
message: `Failed to create ${path}; it may already exist.${cleanupTemporaryFile(tmp)}`,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function verifySourceBytes(
|
|
182
|
+
path: string,
|
|
183
|
+
expectedBytes: string,
|
|
184
|
+
):
|
|
185
|
+
| { ok: true; bytes: string }
|
|
186
|
+
| { ok: false; diagnostic: ReturnType<typeof pstackConfigDiagnostic> } {
|
|
187
|
+
let currentBytes: string;
|
|
188
|
+
try {
|
|
189
|
+
const st = lstatSync(path);
|
|
190
|
+
if (!st.isFile()) {
|
|
191
|
+
return {
|
|
192
|
+
ok: false,
|
|
193
|
+
diagnostic: pstackConfigDiagnostic(
|
|
194
|
+
"not-regular-file",
|
|
195
|
+
"error",
|
|
196
|
+
`${path} is not a regular file.`,
|
|
197
|
+
),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
currentBytes = readFileSync(path, "utf8");
|
|
201
|
+
} catch {
|
|
202
|
+
return {
|
|
203
|
+
ok: false,
|
|
204
|
+
diagnostic: pstackConfigDiagnostic(
|
|
205
|
+
"config-write-failed",
|
|
206
|
+
"error",
|
|
207
|
+
`Failed to verify ${path}. Nothing was written.`,
|
|
208
|
+
),
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
if (currentBytes !== expectedBytes) {
|
|
212
|
+
return {
|
|
213
|
+
ok: false,
|
|
214
|
+
diagnostic: pstackConfigDiagnostic(
|
|
215
|
+
"config-changed",
|
|
216
|
+
"error",
|
|
217
|
+
`${path} changed after it was loaded. Reload before saving.`,
|
|
218
|
+
),
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
return { ok: true, bytes: currentBytes };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Save a normalized v2 config with 0600 atomic replace. */
|
|
225
|
+
export function savePstackRoleConfig(
|
|
226
|
+
config: PstackRoleConfig,
|
|
227
|
+
path: string = configPath(),
|
|
228
|
+
): PstackConfigWriteResult {
|
|
229
|
+
const written = writeAtomicUtf8File(path, serializePstackRoleConfig(config));
|
|
230
|
+
if (written.ok) return { ok: true, path };
|
|
231
|
+
return {
|
|
232
|
+
ok: false,
|
|
233
|
+
diagnostics: [pstackConfigDiagnostic("config-write-failed", "error", written.message)],
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** Create a missing config or replace an existing config only if its bytes are unchanged. */
|
|
238
|
+
export function savePstackRoleConfigIfUnchanged(input: {
|
|
239
|
+
config: PstackRoleConfig;
|
|
240
|
+
path: string;
|
|
241
|
+
originalBytes?: string;
|
|
242
|
+
}): PstackConfigWriteResult {
|
|
243
|
+
if (input.originalBytes === undefined) {
|
|
244
|
+
const created = writeNewAtomicUtf8File(input.path, serializePstackRoleConfig(input.config));
|
|
245
|
+
if (created.ok) return { ok: true, path: input.path };
|
|
246
|
+
return {
|
|
247
|
+
ok: false,
|
|
248
|
+
diagnostics: [pstackConfigDiagnostic("config-changed", "error", created.message)],
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
const verified = verifySourceBytes(input.path, input.originalBytes);
|
|
252
|
+
if (!verified.ok) return { ok: false, diagnostics: [verified.diagnostic] };
|
|
253
|
+
return savePstackRoleConfig(input.config, input.path);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** Exclusive 0600 backup of verified source bytes, then atomic v2 save. */
|
|
257
|
+
export function backupPstackConfigThenSave(input: {
|
|
258
|
+
originalBytes: string;
|
|
259
|
+
config: PstackRoleConfig;
|
|
260
|
+
path: string;
|
|
261
|
+
sourcePath?: string;
|
|
262
|
+
backupPath?: string;
|
|
263
|
+
}): PstackConfigWriteResult {
|
|
264
|
+
const sourcePath = input.sourcePath ?? input.path;
|
|
265
|
+
const backupPath = input.backupPath ?? `${sourcePath}.bak`;
|
|
266
|
+
const verified = verifySourceBytes(sourcePath, input.originalBytes);
|
|
267
|
+
if (!verified.ok) return { ok: false, diagnostics: [verified.diagnostic] };
|
|
268
|
+
|
|
269
|
+
try {
|
|
270
|
+
writeFileSync(backupPath, verified.bytes, { encoding: "utf8", mode: 0o600, flag: "wx" });
|
|
271
|
+
} catch {
|
|
272
|
+
return {
|
|
273
|
+
ok: false,
|
|
274
|
+
diagnostics: [
|
|
275
|
+
pstackConfigDiagnostic(
|
|
276
|
+
"config-write-failed",
|
|
277
|
+
"error",
|
|
278
|
+
`Failed to create exclusive backup ${backupPath}. Left ${input.path} unchanged.`,
|
|
279
|
+
),
|
|
280
|
+
],
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const saved = savePstackRoleConfig(input.config, input.path);
|
|
285
|
+
if (!saved.ok) return saved;
|
|
286
|
+
return { ok: true, path: input.path, backupPath };
|
|
287
|
+
}
|