create-principles-disciple 1.131.2 → 1.132.1
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/console/dist/server/routes/update.js +183 -35
- package/dist/installer.d.ts +36 -0
- package/dist/installer.d.ts.map +1 -1
- package/dist/installer.js +158 -1
- package/dist/installer.js.map +1 -1
- package/dist/update/transaction-journal.d.ts +10 -0
- package/dist/update/transaction-journal.d.ts.map +1 -1
- package/dist/update/transaction-journal.js +13 -0
- package/dist/update/transaction-journal.js.map +1 -1
- package/host-runtime/dist/evaluator-runtime-context.d.ts +63 -0
- package/host-runtime/dist/evaluator-runtime-context.js +61 -0
- package/host-runtime/dist/index.d.ts +1 -0
- package/host-runtime/dist/index.js +2 -0
- package/host-runtime/package.json +1 -1
- package/package.json +2 -2
- package/pd-cli/dist/commands/health.d.ts.map +1 -1
- package/pd-cli/dist/commands/health.js +39 -0
- package/pd-cli/dist/commands/health.js.map +1 -1
- package/pd-cli/dist/commands/runtime-internalization-run-once.d.ts.map +1 -1
- package/pd-cli/dist/commands/runtime-internalization-run-once.js +41 -1
- package/pd-cli/dist/commands/runtime-internalization-run-once.js.map +1 -1
- package/pd-cli/dist/services/rulehost-pipeline-runner.d.ts.map +1 -1
- package/pd-cli/dist/services/rulehost-pipeline-runner.js +22 -1
- package/pd-cli/dist/services/rulehost-pipeline-runner.js.map +1 -1
- package/pd-cli/package.json +1 -1
- package/plugin/dist/bundle.js +140 -140
|
@@ -713,8 +713,148 @@ function depsMeaningfullyChanged(oldDeps, newDeps) {
|
|
|
713
713
|
return aKeys.some((k, i) => bKeys[i] !== k || a[k] !== b[k]);
|
|
714
714
|
}
|
|
715
715
|
/**
|
|
716
|
-
*
|
|
717
|
-
*
|
|
716
|
+
* Move the current occupant of a dependency slot aside (same-directory
|
|
717
|
+
* rename: same volume, atomic, reversible). The quarantine name starts with
|
|
718
|
+
* a dot and is not a valid package name, so npm module resolution ignores it.
|
|
719
|
+
*/
|
|
720
|
+
function quarantineSlot(slot) {
|
|
721
|
+
try {
|
|
722
|
+
const quarantinePath = path.join(path.dirname(slot), `.${path.basename(slot)}.update-quarantine-${Date.now()}`);
|
|
723
|
+
fs.renameSync(slot, quarantinePath);
|
|
724
|
+
return { slot, quarantinePath };
|
|
725
|
+
}
|
|
726
|
+
catch {
|
|
727
|
+
return undefined;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
/**
|
|
731
|
+
* Restore quarantined slots to their original locations (update failure
|
|
732
|
+
* path). The slot at that point holds the link reconciliation created —
|
|
733
|
+
* unlink removes the link itself, never its target. Best-effort, never
|
|
734
|
+
* throws; failures are logged (rc-9: observable degradation).
|
|
735
|
+
*/
|
|
736
|
+
function restoreQuarantined(quarantined) {
|
|
737
|
+
for (const entry of [...quarantined].reverse()) {
|
|
738
|
+
try {
|
|
739
|
+
try {
|
|
740
|
+
fs.unlinkSync(entry.slot);
|
|
741
|
+
}
|
|
742
|
+
catch { /* slot may already be gone */ }
|
|
743
|
+
fs.renameSync(entry.quarantinePath, entry.slot);
|
|
744
|
+
}
|
|
745
|
+
catch (error) {
|
|
746
|
+
console.error(`[pd-console:update] failed to restore quarantined dependency entry ${entry.slot}: ${error instanceof Error ? error.message : String(error)}`);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
/**
|
|
751
|
+
* Discard quarantined slots (update success path). The quarantined entries
|
|
752
|
+
* are stale pre-update copies of internal @principles packages; the new
|
|
753
|
+
* canonical components are installed by the copy steps. Best-effort,
|
|
754
|
+
* failures are logged and non-fatal (rc-9).
|
|
755
|
+
*/
|
|
756
|
+
function cleanupQuarantined(quarantined) {
|
|
757
|
+
for (const entry of quarantined) {
|
|
758
|
+
try {
|
|
759
|
+
fs.rmSync(entry.quarantinePath, { recursive: true, force: true });
|
|
760
|
+
}
|
|
761
|
+
catch (error) {
|
|
762
|
+
console.warn(`[pd-console:update] failed to clean quarantined dependency entry ${entry.quarantinePath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
/**
|
|
767
|
+
* True when the link at linkPath resolves to exactly `target` (string
|
|
768
|
+
* comparison — the target dir may legitimately not exist yet at
|
|
769
|
+
* reconciliation time). Windows paths compare case-insensitively.
|
|
770
|
+
*/
|
|
771
|
+
function linkPointsAt(linkPath, target) {
|
|
772
|
+
const resolved = path.resolve(path.dirname(linkPath), fs.readlinkSync(linkPath));
|
|
773
|
+
const expected = path.resolve(target);
|
|
774
|
+
return process.platform === 'win32'
|
|
775
|
+
? resolved.toLowerCase() === expected.toLowerCase()
|
|
776
|
+
: resolved === expected;
|
|
777
|
+
}
|
|
778
|
+
function createResolutionLink(linkPath, target) {
|
|
779
|
+
try {
|
|
780
|
+
fs.mkdirSync(path.dirname(linkPath), { recursive: true });
|
|
781
|
+
if (process.platform === 'win32') {
|
|
782
|
+
fs.symlinkSync(target, linkPath, 'junction');
|
|
783
|
+
}
|
|
784
|
+
else {
|
|
785
|
+
fs.symlinkSync(path.relative(path.dirname(linkPath), target), linkPath, 'dir');
|
|
786
|
+
}
|
|
787
|
+
return undefined;
|
|
788
|
+
}
|
|
789
|
+
catch (error) {
|
|
790
|
+
return `Failed to create runtime resolution link at ${linkPath}: ${error instanceof Error ? error.message : String(error)}`;
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
/**
|
|
794
|
+
* Reconcile ONE deployed dependency slot against its canonical target
|
|
795
|
+
* (PRI-665, 2026-09-03 incident). The previous "existsSync → skip" logic
|
|
796
|
+
* silently kept stale PHYSICAL copies of internal @principles packages that
|
|
797
|
+
* legacy installs had left in these slots, so the updated dist resolved the
|
|
798
|
+
* old components and crashed at startup. Semantics:
|
|
799
|
+
*
|
|
800
|
+
* - missing slot → create the link (fresh installs);
|
|
801
|
+
* - link pointing at target → keep (npm- or installer-created);
|
|
802
|
+
* - wrong-target link → quarantine + replace;
|
|
803
|
+
* - physical dir / plain file → quarantine + replace (the incident shape).
|
|
804
|
+
*
|
|
805
|
+
* Returns an error message on failure (the slot is rolled back first), or
|
|
806
|
+
* undefined; a successful quarantine is recorded in `quarantined` for the
|
|
807
|
+
* caller to restore on failure / discard on success.
|
|
808
|
+
*/
|
|
809
|
+
function reconcileResolutionLink(linkPath, target, quarantined) {
|
|
810
|
+
let stat;
|
|
811
|
+
try {
|
|
812
|
+
stat = fs.lstatSync(linkPath);
|
|
813
|
+
}
|
|
814
|
+
catch {
|
|
815
|
+
stat = undefined; // ENOENT — create below
|
|
816
|
+
}
|
|
817
|
+
if (stat === undefined) {
|
|
818
|
+
return createResolutionLink(linkPath, target);
|
|
819
|
+
}
|
|
820
|
+
if (stat.isSymbolicLink()) {
|
|
821
|
+
try {
|
|
822
|
+
if (linkPointsAt(linkPath, target))
|
|
823
|
+
return undefined; // correct link — keep
|
|
824
|
+
}
|
|
825
|
+
catch {
|
|
826
|
+
// Unreadable link target — fall through to replace.
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
const entry = quarantineSlot(linkPath);
|
|
830
|
+
if (entry === undefined) {
|
|
831
|
+
return `Failed to quarantine the existing dependency entry at ${linkPath} (required to install the canonical resolution link). Resolve any file locks and retry the update.`;
|
|
832
|
+
}
|
|
833
|
+
const error = createResolutionLink(linkPath, target);
|
|
834
|
+
if (error) {
|
|
835
|
+
// Roll this slot back before failing — the install stays untouched
|
|
836
|
+
// (the PRI-561 fail-closed ordering contract).
|
|
837
|
+
try {
|
|
838
|
+
fs.unlinkSync(linkPath);
|
|
839
|
+
}
|
|
840
|
+
catch { /* nothing we created */ }
|
|
841
|
+
try {
|
|
842
|
+
fs.renameSync(entry.quarantinePath, entry.slot);
|
|
843
|
+
}
|
|
844
|
+
catch {
|
|
845
|
+
// The in-slot rename failed (e.g. a transient lock). Hand the entry to
|
|
846
|
+
// the outer rollback so it retries the restore — restoreQuarantined
|
|
847
|
+
// tolerates a missing slot when unlinking before renaming back.
|
|
848
|
+
quarantined.push(entry);
|
|
849
|
+
}
|
|
850
|
+
return error;
|
|
851
|
+
}
|
|
852
|
+
quarantined.push(entry);
|
|
853
|
+
return undefined;
|
|
854
|
+
}
|
|
855
|
+
/**
|
|
856
|
+
* Create or RECONCILE the node_modules resolution links for the installed
|
|
857
|
+
* components.
|
|
718
858
|
*
|
|
719
859
|
* Mirrors installer.ts syncPdCli: junction on Windows (no elevation needed),
|
|
720
860
|
* relative symlink elsewhere. Fresh installs get these links via npm install
|
|
@@ -738,8 +878,11 @@ function depsMeaningfullyChanged(oldDeps, newDeps) {
|
|
|
738
878
|
* host-runtime/node_modules/@principles/install-layout missing and
|
|
739
879
|
* every pd-cli runtime command failing with ERR_MODULE_NOT_FOUND.
|
|
740
880
|
*
|
|
741
|
-
* Returns
|
|
742
|
-
* silent — a missing link means the
|
|
881
|
+
* Returns { error } on failure with every quarantine rolled back (rc-9:
|
|
882
|
+
* observable, never silent — a missing or unreconciled link means the
|
|
883
|
+
* updated console cannot start), or { quarantined } listing the dependency
|
|
884
|
+
* entries replaced during reconciliation. The caller restores the
|
|
885
|
+
* quarantined entries if a LATER step fails, and discards them on success.
|
|
743
886
|
*/
|
|
744
887
|
function ensureRuntimeResolutionLinks(layout, tempDir) {
|
|
745
888
|
const links = [
|
|
@@ -784,47 +927,35 @@ function ensureRuntimeResolutionLinks(layout, tempDir) {
|
|
|
784
927
|
}
|
|
785
928
|
};
|
|
786
929
|
const fileDepSpecs = collectFileDepLinkSpecs(stagedComponents, readStagedDependencies);
|
|
787
|
-
const
|
|
788
|
-
// Idempotent: never overwrite an existing link or directory (fresh
|
|
789
|
-
// installs have npm-created real dirs in these slots).
|
|
790
|
-
if (fs.existsSync(linkPath))
|
|
791
|
-
return undefined;
|
|
792
|
-
try {
|
|
793
|
-
fs.mkdirSync(path.dirname(linkPath), { recursive: true });
|
|
794
|
-
if (process.platform === 'win32') {
|
|
795
|
-
fs.symlinkSync(target, linkPath, 'junction');
|
|
796
|
-
}
|
|
797
|
-
else {
|
|
798
|
-
fs.symlinkSync(path.relative(path.dirname(linkPath), target), linkPath, 'dir');
|
|
799
|
-
}
|
|
800
|
-
return undefined;
|
|
801
|
-
}
|
|
802
|
-
catch (error) {
|
|
803
|
-
return `Failed to create runtime resolution link at ${linkPath}: ${error instanceof Error ? error.message : String(error)}`;
|
|
804
|
-
}
|
|
805
|
-
};
|
|
930
|
+
const quarantined = [];
|
|
806
931
|
// Pass 1 — explicit known-critical links, fail-closed BEFORE any byte is
|
|
807
|
-
// swapped (a link-
|
|
808
|
-
// untouched: the PRI-561 ordering contract).
|
|
932
|
+
// swapped (a link-reconciliation failure aborts with the installed packages
|
|
933
|
+
// untouched: the PRI-561 ordering contract). Existing entries are
|
|
934
|
+
// RECONCILED, not skipped: a correct link is kept, but a stale physical
|
|
935
|
+
// copy or a wrong-target link is quarantined and replaced (PRI-665,
|
|
936
|
+
// 2026-09-03: legacy installs left physical @principles copies in these
|
|
937
|
+
// slots and updated dists crashed resolving them).
|
|
809
938
|
for (const { linkPath, target } of links) {
|
|
810
939
|
if (!fs.existsSync(target))
|
|
811
940
|
continue;
|
|
812
|
-
const error =
|
|
813
|
-
if (error)
|
|
814
|
-
|
|
941
|
+
const error = reconcileResolutionLink(linkPath, target, quarantined);
|
|
942
|
+
if (error) {
|
|
943
|
+
restoreQuarantined(quarantined);
|
|
944
|
+
return { error, quarantined: [] };
|
|
945
|
+
}
|
|
815
946
|
}
|
|
816
947
|
// Pass 2 — data-driven derived links. Their deployed target dir may be
|
|
817
948
|
// created by the copy steps that follow (a brand-new component's dir does
|
|
818
949
|
// not exist yet); each staged target's existence was already proven by the
|
|
819
950
|
// extraction, and the copies below run unconditionally.
|
|
820
951
|
for (const spec of fileDepSpecs) {
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
952
|
+
const error = reconcileResolutionLink(spec.linkPath, spec.target, quarantined);
|
|
953
|
+
if (error) {
|
|
954
|
+
restoreQuarantined(quarantined);
|
|
955
|
+
return { error, quarantined: [] };
|
|
956
|
+
}
|
|
826
957
|
}
|
|
827
|
-
return
|
|
958
|
+
return { quarantined };
|
|
828
959
|
}
|
|
829
960
|
async function doInlineFullUpdate(workspaceDir) {
|
|
830
961
|
const layout = resolveUpdateLayout();
|
|
@@ -861,6 +992,9 @@ async function doInlineFullUpdate(workspaceDir) {
|
|
|
861
992
|
// overwrites it (PR #1332 companion — see reapplySkillLanguage).
|
|
862
993
|
const skillLanguage = detectInstalledSkillLanguage(extDir);
|
|
863
994
|
let tempDir;
|
|
995
|
+
// Dependency entries quarantined during resolution-link reconciliation;
|
|
996
|
+
// restored on failure, discarded on success (PRI-665).
|
|
997
|
+
let reconciledQuarantine = [];
|
|
864
998
|
try {
|
|
865
999
|
// 2. Fetch installer package info from npm
|
|
866
1000
|
const response = await fetchWithRetry(NPM_REGISTRY_INSTALLER, 'Installer registry check');
|
|
@@ -1018,7 +1152,8 @@ async function doInlineFullUpdate(workspaceDir) {
|
|
|
1018
1152
|
if (fs.existsSync(path.join(installLayoutSrc, 'package.json')) && fs.existsSync(path.join(installLayoutSrc, 'dist'))) {
|
|
1019
1153
|
copyDirRecursive(installLayoutSrc, layout.installLayoutDir, SKIP_DIRS);
|
|
1020
1154
|
}
|
|
1021
|
-
const linkError = ensureRuntimeResolutionLinks(layout, tempDir);
|
|
1155
|
+
const { error: linkError, quarantined } = ensureRuntimeResolutionLinks(layout, tempDir);
|
|
1156
|
+
reconciledQuarantine = quarantined;
|
|
1022
1157
|
if (linkError) {
|
|
1023
1158
|
appendUpdateHistory(workspaceDir, {
|
|
1024
1159
|
fromVersion,
|
|
@@ -1078,6 +1213,13 @@ async function doInlineFullUpdate(workspaceDir) {
|
|
|
1078
1213
|
if (tempDir && fs.existsSync(tempDir)) {
|
|
1079
1214
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
1080
1215
|
}
|
|
1216
|
+
// 6.5 Discard the quarantined stale dependency copies — the canonical
|
|
1217
|
+
// components are in place now (PRI-665). Both the success return and the
|
|
1218
|
+
// version-drift failure return flow through here with files already
|
|
1219
|
+
// swapped, so restoring the stale copies would re-break resolution.
|
|
1220
|
+
if (reconciledQuarantine.length > 0) {
|
|
1221
|
+
cleanupQuarantined(reconciledQuarantine);
|
|
1222
|
+
}
|
|
1081
1223
|
// 7. Version-advance check (drift guard). The full update installs the
|
|
1082
1224
|
// plugin bundled inside the installer. If the installer is stale (its
|
|
1083
1225
|
// bundled plugin is NOT newer than what is installed), the "update" is a
|
|
@@ -1122,6 +1264,12 @@ async function doInlineFullUpdate(workspaceDir) {
|
|
|
1122
1264
|
};
|
|
1123
1265
|
}
|
|
1124
1266
|
catch (error) {
|
|
1267
|
+
// PRI-665: restore any dependency slots quarantined during link
|
|
1268
|
+
// reconciliation FIRST — a failed update must not leave the install
|
|
1269
|
+
// half-migrated (new dist with the old resolution quarantined away).
|
|
1270
|
+
if (reconciledQuarantine.length > 0) {
|
|
1271
|
+
restoreQuarantined(reconciledQuarantine);
|
|
1272
|
+
}
|
|
1125
1273
|
// Clean up temp dir on failure
|
|
1126
1274
|
if (tempDir && fs.existsSync(tempDir)) {
|
|
1127
1275
|
try {
|
package/dist/installer.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { type ComponentStatus, type VerificationResult } from './mvp-config.js';
|
|
|
3
3
|
import { type HostTarget } from './installers/index.js';
|
|
4
4
|
import type { HostInstallResult } from '@principles/core/host';
|
|
5
5
|
import { type SkillLanguage } from './skill-language.js';
|
|
6
|
+
import { type ReleaseMetadataDigestSource, type TransactionState } from './update/transaction-journal.js';
|
|
6
7
|
/** PRI-343: Keep in sync with @principles/core CONVERSATION_ACCESS_CONFIG_KEY */
|
|
7
8
|
export declare const CONVERSATION_ACCESS_CONFIG_KEY: 'allowConversationAccess';
|
|
8
9
|
/**
|
|
@@ -137,6 +138,19 @@ export interface InstallResult {
|
|
|
137
138
|
consoleUrl?: string;
|
|
138
139
|
/** ADR-0020 §2.3: Host-side install results (one per HostInstaller). */
|
|
139
140
|
hostResults?: HostInstallResult[];
|
|
141
|
+
/** ADR-0024 D-2 (PRI-664): transaction journal record for this install.
|
|
142
|
+
* Undefined when the mutation never began (pre-mutation refusal/throw). */
|
|
143
|
+
journal?: InstallJournalRecord;
|
|
144
|
+
}
|
|
145
|
+
/** Observability record for one installer transaction (ADR-0024 D-2). */
|
|
146
|
+
export interface InstallJournalRecord {
|
|
147
|
+
readonly transactionId: string;
|
|
148
|
+
/** Journal file: `~/.pd/transactions/<transactionId>.jsonl` (D-6, runtime scope). */
|
|
149
|
+
readonly journalPath: string;
|
|
150
|
+
/** True when a mid-flight journal append failed (Tier-2 degradation) and
|
|
151
|
+
* later transitions were skipped — the backup/restore safety net remained
|
|
152
|
+
* authoritative for this transaction. */
|
|
153
|
+
readonly degraded: boolean;
|
|
140
154
|
}
|
|
141
155
|
export interface InstallRunMode {
|
|
142
156
|
/** Suppress human output (spinner / progress). True under --json. */
|
|
@@ -144,6 +158,28 @@ export interface InstallRunMode {
|
|
|
144
158
|
/** No interactive prompts. True under --yes / --non-interactive / --json. */
|
|
145
159
|
nonInteractive?: boolean;
|
|
146
160
|
}
|
|
161
|
+
interface InstallerJournal {
|
|
162
|
+
readonly transactionId: string;
|
|
163
|
+
readonly journalPath: string;
|
|
164
|
+
readonly releaseId: string;
|
|
165
|
+
readonly productVersion: string;
|
|
166
|
+
readonly releaseMetadataDigest: string;
|
|
167
|
+
/** PRI-664 review: provenance of releaseMetadataDigest ('manifest' | 'package_manifest' | 'fallback'). */
|
|
168
|
+
readonly releaseMetadataDigestSource: ReleaseMetadataDigestSource;
|
|
169
|
+
degraded: boolean;
|
|
170
|
+
/** Last successfully journaled state — the `from` for failure-path transitions. */
|
|
171
|
+
lastState: TransactionState | null;
|
|
172
|
+
}
|
|
173
|
+
/** Opens one installer transaction: `~/.pd/transactions/<transactionId>.jsonl`. */
|
|
174
|
+
export declare function beginInstallerJournal(pluginDir: string): InstallerJournal;
|
|
175
|
+
/**
|
|
176
|
+
* Append one transition durably (append + fsync) BEFORE the side effect it
|
|
177
|
+
* describes. Throws on failure — the caller decides Tier-1 (refuse before
|
|
178
|
+
* mutation) vs Tier-2 (degrade and continue).
|
|
179
|
+
*/
|
|
180
|
+
export declare function journalInstallerTransition(journal: InstallerJournal, from: TransactionState | null, to: TransactionState, detail: string): void;
|
|
181
|
+
/** Tier-2 wrapper: on append failure, degrade (mark + warn) instead of throwing. */
|
|
182
|
+
export declare function journalInstallerTransitionDegrading(journal: InstallerJournal, from: TransactionState | null, to: TransactionState, detail: string): void;
|
|
147
183
|
export declare function install(options: InstallOptions, pluginDir: string, mode?: InstallRunMode): Promise<InstallResult>;
|
|
148
184
|
export {};
|
|
149
185
|
//# sourceMappingURL=installer.d.ts.map
|
package/dist/installer.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"installer.d.ts","sourceRoot":"","sources":["../src/installer.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"installer.d.ts","sourceRoot":"","sources":["../src/installer.ts"],"names":[],"mappings":"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"}
|
package/dist/installer.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { existsSync, readdirSync, statSync, readFileSync, writeFileSync, mkdirSync, rmSync, copyFileSync, cpSync, renameSync, chmodSync, symlinkSync } from 'fs';
|
|
2
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
3
|
import fse from 'fs-extra';
|
|
3
4
|
import * as path from 'path';
|
|
4
5
|
import * as http from 'http';
|
|
@@ -13,6 +14,7 @@ import { getHostInstallers } from './installers/index.js';
|
|
|
13
14
|
import { mergeInstallManifestWorkspaces, parseInstallManifest } from '@principles/install-layout';
|
|
14
15
|
import { applySkillLanguageSelection } from './skill-language.js';
|
|
15
16
|
import { parseReleaseAssetIdentity, parseReleaseAssetManifest, ReleaseAssetManifestError, verifyReleaseAssetManifestAsync, verifyReleaseAssetTarget, } from './update/release-asset-manifest.js';
|
|
17
|
+
import { appendJournalTransition } from './update/transaction-journal.js';
|
|
16
18
|
/** PRI-343: Keep in sync with @principles/core CONVERSATION_ACCESS_CONFIG_KEY */
|
|
17
19
|
export const CONVERSATION_ACCESS_CONFIG_KEY = 'allowConversationAccess';
|
|
18
20
|
function isRecord(value) {
|
|
@@ -1645,6 +1647,98 @@ async function runHostInstallers(host, ctx) {
|
|
|
1645
1647
|
}
|
|
1646
1648
|
return results;
|
|
1647
1649
|
}
|
|
1650
|
+
function sha256File(filePath) {
|
|
1651
|
+
return createHash('sha256').update(readFileSync(filePath)).digest('hex');
|
|
1652
|
+
}
|
|
1653
|
+
/**
|
|
1654
|
+
* Identity of the payload being installed. Prefers the self-contained asset
|
|
1655
|
+
* manifest (covers the whole payload); falls back to the bundled pd-cli
|
|
1656
|
+
* package manifest. Both are REAL digests — the journal never stores a
|
|
1657
|
+
* placeholder where a verifiable value is available (same discipline as
|
|
1658
|
+
* legacy-migration.ts). The last-resort fallback hashes the literal reason
|
|
1659
|
+
* string only to satisfy the journal's 64-hex format requirement; it is not
|
|
1660
|
+
* part of any release-metadata identity chain.
|
|
1661
|
+
*/
|
|
1662
|
+
function resolveInstallerPayloadIdentity(pluginDir) {
|
|
1663
|
+
let productVersion = 'unknown';
|
|
1664
|
+
const pdCliPkgPath = path.join(pluginDir, 'pd-cli', 'package.json');
|
|
1665
|
+
if (existsSync(pdCliPkgPath)) {
|
|
1666
|
+
try {
|
|
1667
|
+
const parsed = JSON.parse(readFileSync(pdCliPkgPath, 'utf8'));
|
|
1668
|
+
if (typeof parsed.version === 'string' && parsed.version.length > 0)
|
|
1669
|
+
productVersion = parsed.version;
|
|
1670
|
+
}
|
|
1671
|
+
catch {
|
|
1672
|
+
// Identity falls back to 'unknown'; journaling must not brick install.
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
const assetManifestPath = path.join(pluginDir, '_release', 'manifest.json');
|
|
1676
|
+
let releaseMetadataDigest;
|
|
1677
|
+
let releaseMetadataDigestSource;
|
|
1678
|
+
if (existsSync(assetManifestPath)) {
|
|
1679
|
+
releaseMetadataDigest = sha256File(assetManifestPath);
|
|
1680
|
+
releaseMetadataDigestSource = 'manifest';
|
|
1681
|
+
}
|
|
1682
|
+
else if (existsSync(pdCliPkgPath)) {
|
|
1683
|
+
releaseMetadataDigest = sha256File(pdCliPkgPath);
|
|
1684
|
+
releaseMetadataDigestSource = 'package_manifest';
|
|
1685
|
+
}
|
|
1686
|
+
else {
|
|
1687
|
+
releaseMetadataDigest = createHash('sha256').update('installer-payload-missing-identity').digest('hex');
|
|
1688
|
+
releaseMetadataDigestSource = 'fallback';
|
|
1689
|
+
}
|
|
1690
|
+
return { productVersion, releaseMetadataDigest, releaseMetadataDigestSource };
|
|
1691
|
+
}
|
|
1692
|
+
/** Opens one installer transaction: `~/.pd/transactions/<transactionId>.jsonl`. */
|
|
1693
|
+
export function beginInstallerJournal(pluginDir) {
|
|
1694
|
+
const { productVersion, releaseMetadataDigest, releaseMetadataDigestSource } = resolveInstallerPayloadIdentity(pluginDir);
|
|
1695
|
+
const transactionId = `install-${Date.now()}-${randomUUID().slice(0, 8)}`;
|
|
1696
|
+
const journalPath = path.join(getPdDir(), 'transactions', `${transactionId}.jsonl`);
|
|
1697
|
+
const releaseId = `bundled-${productVersion}-${releaseMetadataDigest.slice(0, 12)}`;
|
|
1698
|
+
return { transactionId, journalPath, releaseId, productVersion, releaseMetadataDigest, releaseMetadataDigestSource, degraded: false, lastState: null };
|
|
1699
|
+
}
|
|
1700
|
+
/**
|
|
1701
|
+
* Append one transition durably (append + fsync) BEFORE the side effect it
|
|
1702
|
+
* describes. Throws on failure — the caller decides Tier-1 (refuse before
|
|
1703
|
+
* mutation) vs Tier-2 (degrade and continue).
|
|
1704
|
+
*/
|
|
1705
|
+
// eslint-disable-next-line @typescript-eslint/max-params -- (from, to, detail) mirrors the JournalTransition shape it appends
|
|
1706
|
+
export function journalInstallerTransition(journal, from, to, detail) {
|
|
1707
|
+
appendJournalTransition(journal.journalPath, {
|
|
1708
|
+
at: new Date().toISOString(),
|
|
1709
|
+
from,
|
|
1710
|
+
to,
|
|
1711
|
+
transactionId: journal.transactionId,
|
|
1712
|
+
releaseId: journal.releaseId,
|
|
1713
|
+
productVersion: journal.productVersion,
|
|
1714
|
+
releaseMetadataDigest: journal.releaseMetadataDigest,
|
|
1715
|
+
releaseMetadataDigestSource: journal.releaseMetadataDigestSource,
|
|
1716
|
+
// The installer model has no active.json generation chain yet (it never
|
|
1717
|
+
// writes active.json); generation continuity becomes ReleaseManager's
|
|
1718
|
+
// responsibility when it takes over mutations (PRI-661).
|
|
1719
|
+
generation: 1,
|
|
1720
|
+
detail,
|
|
1721
|
+
});
|
|
1722
|
+
journal.lastState = to;
|
|
1723
|
+
}
|
|
1724
|
+
/** Tier-2 wrapper: on append failure, degrade (mark + warn) instead of throwing. */
|
|
1725
|
+
// eslint-disable-next-line @typescript-eslint/max-params -- (from, to, detail) mirrors the JournalTransition shape it appends
|
|
1726
|
+
export function journalInstallerTransitionDegrading(journal, from, to, detail) {
|
|
1727
|
+
if (journal.degraded)
|
|
1728
|
+
return;
|
|
1729
|
+
try {
|
|
1730
|
+
journalInstallerTransition(journal, from, to, detail);
|
|
1731
|
+
}
|
|
1732
|
+
catch (error) {
|
|
1733
|
+
journal.degraded = true;
|
|
1734
|
+
logger.error(`Transaction journal append failed at '${to}' — continuing under the backup/restore safety net; `
|
|
1735
|
+
+ `this transaction is partially journaled (ADR-0024 D-2 Tier-2 degradation): `
|
|
1736
|
+
+ `${error instanceof Error ? error.message : String(error)}`);
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
function installerJournalRecord(journal) {
|
|
1740
|
+
return { transactionId: journal.transactionId, journalPath: journal.journalPath, degraded: journal.degraded };
|
|
1741
|
+
}
|
|
1648
1742
|
export async function install(options, pluginDir, mode = {}) {
|
|
1649
1743
|
// `quiet` (= jsonMode) suppresses human output / spinner. `nonInteractive`
|
|
1650
1744
|
// (--yes/--non-interactive/--json) gates PROMPTING. They differ for `--yes`
|
|
@@ -1726,6 +1820,8 @@ export async function install(options, pluginDir, mode = {}) {
|
|
|
1726
1820
|
const spinner = quiet ? null : ora('Installing...').start();
|
|
1727
1821
|
let backupDir = null;
|
|
1728
1822
|
let runtimeBackupDir = null;
|
|
1823
|
+
// ADR-0024 D-2: non-null once the transaction is planned (first mutation is imminent).
|
|
1824
|
+
let journal = null;
|
|
1729
1825
|
let installManifestHosts;
|
|
1730
1826
|
const components = { plugin: 'skipped', cli: 'skipped', console: 'skipped' };
|
|
1731
1827
|
const verification = { features: 'skipped', storyA: 'skipped' };
|
|
@@ -1767,6 +1863,34 @@ export async function install(options, pluginDir, mode = {}) {
|
|
|
1767
1863
|
stepIndex++;
|
|
1768
1864
|
if (spinner)
|
|
1769
1865
|
updateProgress(spinner, stepIndex, 'Backing up existing install...');
|
|
1866
|
+
// ADR-0024 D-2 (PRI-664): journal-first — record 'planned' BEFORE the
|
|
1867
|
+
// first runtime mutation (the backup rename). Tier-1 policy: if the
|
|
1868
|
+
// journal cannot be written at all, refuse before mutating (fail loud,
|
|
1869
|
+
// zero side effects) rather than performing an unjournaled mutation
|
|
1870
|
+
// (refusal over silent degradation, ADR-0024 §2.4 rule 4).
|
|
1871
|
+
journal = beginInstallerJournal(pluginDir);
|
|
1872
|
+
try {
|
|
1873
|
+
journalInstallerTransition(journal, null, 'planned', `host=${options.host} mode=${options.mode}`);
|
|
1874
|
+
}
|
|
1875
|
+
catch (journalError) {
|
|
1876
|
+
if (spinner)
|
|
1877
|
+
spinner.fail('Install failed');
|
|
1878
|
+
const journalDetail = journalError instanceof Error ? journalError.message : String(journalError);
|
|
1879
|
+
logger.error(`Transaction journal unavailable — refusing to mutate the runtime unjournaled (ADR-0024 D-2): ${journalDetail}`);
|
|
1880
|
+
return {
|
|
1881
|
+
success: false,
|
|
1882
|
+
workspaceDir: options.workspaceDir,
|
|
1883
|
+
configYamlPath: getConfigYamlPath(options.workspaceDir),
|
|
1884
|
+
templatesCount: 0,
|
|
1885
|
+
components,
|
|
1886
|
+
verification,
|
|
1887
|
+
enabledChannels: options.channels,
|
|
1888
|
+
nextAction: 'Resolve write access to ~/.pd/transactions (disk space / permissions), then re-run the installer. No changes were made.',
|
|
1889
|
+
reason: `transaction_journal_unavailable: ${journalDetail}`,
|
|
1890
|
+
error: `Could not write the transaction journal — refusing to mutate the runtime unjournaled (ADR-0024 D-2). No changes were made.`,
|
|
1891
|
+
journal: { transactionId: journal.transactionId, journalPath: journal.journalPath, degraded: true },
|
|
1892
|
+
};
|
|
1893
|
+
}
|
|
1770
1894
|
// Validate the existing host-ownership record before mutating runtime or
|
|
1771
1895
|
// host config. A malformed manifest must not be discovered only after the
|
|
1772
1896
|
// old installation has already been replaced (rc-3/rc-9).
|
|
@@ -1847,6 +1971,9 @@ export async function install(options, pluginDir, mode = {}) {
|
|
|
1847
1971
|
updateProgress(spinner, stepIndex, 'Validating bundled console dependencies...');
|
|
1848
1972
|
await installConsoleDependencies();
|
|
1849
1973
|
stepIndex++;
|
|
1974
|
+
// ADR-0024 D-2: all runtime content is laid down — the new installation
|
|
1975
|
+
// is staged (nothing has been discarded yet; backups still hold the old one).
|
|
1976
|
+
journalInstallerTransitionDegrading(journal, journal.lastState, 'staged', 'runtime components installed');
|
|
1850
1977
|
if (spinner)
|
|
1851
1978
|
updateProgress(spinner, stepIndex, 'Verifying pd-console...');
|
|
1852
1979
|
const consoleVerify = await verifyConsole(options.workspaceDir);
|
|
@@ -1859,6 +1986,8 @@ export async function install(options, pluginDir, mode = {}) {
|
|
|
1859
1986
|
throw new Error(`Console verification failed: ${consoleVerify.reason ?? 'unknown'}. Installation rolled back — plugin and CLI are not activated.`);
|
|
1860
1987
|
}
|
|
1861
1988
|
stepIndex++;
|
|
1989
|
+
// ADR-0024 D-2: the console probe passed — the staged installation is live.
|
|
1990
|
+
journalInstallerTransitionDegrading(journal, journal.lastState, 'probed', `console verified at ${consoleVerify.url}`);
|
|
1862
1991
|
if (spinner)
|
|
1863
1992
|
updateProgress(spinner, stepIndex, 'Copying templates...');
|
|
1864
1993
|
const templatesCount = await copyCoreTemplates({
|
|
@@ -1965,7 +2094,13 @@ export async function install(options, pluginDir, mode = {}) {
|
|
|
1965
2094
|
throw new Error(`Host installation failed: ${hostFailures.join(' | ')}`);
|
|
1966
2095
|
}
|
|
1967
2096
|
writeInstallManifest(installManifestHosts, resolveInstallManifestWorkspaces(options.workspaceDir));
|
|
2097
|
+
// ADR-0024 D-2: host installers completed and the install manifest is
|
|
2098
|
+
// written — the new installation is fully activated (backups not yet
|
|
2099
|
+
// discarded, so a crash here still recovers via the backup).
|
|
2100
|
+
journalInstallerTransitionDegrading(journal, journal.lastState, 'activated', 'host installers completed; install manifest written');
|
|
1968
2101
|
cleanupBackup(backupDir, runtimeBackupDir);
|
|
2102
|
+
// ADR-0024 D-2: backup cleanup is the commit point of the transaction.
|
|
2103
|
+
journalInstallerTransitionDegrading(journal, journal.lastState, 'confirmed', 'backup cleaned up; install complete');
|
|
1969
2104
|
if (spinner) {
|
|
1970
2105
|
spinner.succeed('Install complete!');
|
|
1971
2106
|
}
|
|
@@ -2015,14 +2150,35 @@ export async function install(options, pluginDir, mode = {}) {
|
|
|
2015
2150
|
nextAction: nextActions.join(' | '),
|
|
2016
2151
|
consoleUrl: launchResult.consoleUrl,
|
|
2017
2152
|
hostResults,
|
|
2153
|
+
journal: installerJournalRecord(journal),
|
|
2018
2154
|
};
|
|
2019
2155
|
}
|
|
2020
2156
|
catch (error) {
|
|
2021
2157
|
if (spinner)
|
|
2022
2158
|
spinner.fail('Install failed');
|
|
2023
2159
|
killConsoleChild();
|
|
2160
|
+
// ADR-0024 D-2: record the outcome (journal may be null when the throw
|
|
2161
|
+
// happened before the transaction was planned — zero side effects then).
|
|
2162
|
+
// `failed` and `rolled_back` are BOTH terminal states and one journal
|
|
2163
|
+
// file must end at exactly one terminal state (the strict reader rejects
|
|
2164
|
+
// any transition after a terminal one), so the outcome is either-or:
|
|
2165
|
+
// - backup restored (a real rollback happened) → `rolled_back`
|
|
2166
|
+
// - no backup existed (mutation never started) or restore failed → `failed`
|
|
2167
|
+
const errorMsgRaw = error instanceof Error ? error.message : String(error);
|
|
2024
2168
|
const restoreResult = restoreBackup(backupDir, runtimeBackupDir);
|
|
2025
|
-
|
|
2169
|
+
if (journal) {
|
|
2170
|
+
const restoreFailed = Boolean(backupDir || runtimeBackupDir) && !restoreResult.restored;
|
|
2171
|
+
const detail = restoreFailed
|
|
2172
|
+
? `${errorMsgRaw}; backup restore FAILED: ${restoreResult.error ?? 'unknown'}`
|
|
2173
|
+
: errorMsgRaw;
|
|
2174
|
+
if ((backupDir || runtimeBackupDir) && restoreResult.restored) {
|
|
2175
|
+
journalInstallerTransitionDegrading(journal, journal.lastState, 'rolled_back', detail);
|
|
2176
|
+
}
|
|
2177
|
+
else {
|
|
2178
|
+
journalInstallerTransitionDegrading(journal, journal.lastState, 'failed', detail);
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
const errorMsg = errorMsgRaw;
|
|
2026
2182
|
// ERR-046 / rc-9: never claim a restore that didn't happen. When backupDir
|
|
2027
2183
|
// is null, the backup step never completed (it threw — e.g. EPERM — or
|
|
2028
2184
|
// there was no existing install), so the existing install was never moved
|
|
@@ -2074,6 +2230,7 @@ export async function install(options, pluginDir, mode = {}) {
|
|
|
2074
2230
|
component: error instanceof SelfContainedDependencyError ? error.component : undefined,
|
|
2075
2231
|
dependency: error instanceof SelfContainedDependencyError ? error.dependency : undefined,
|
|
2076
2232
|
error: `${errorMsg} — ${rollbackSuffix}`,
|
|
2233
|
+
journal: journal ? installerJournalRecord(journal) : undefined,
|
|
2077
2234
|
};
|
|
2078
2235
|
}
|
|
2079
2236
|
finally {
|