create-principles-disciple 1.105.0 → 1.107.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/console/dist/server/index.js +16 -0
- package/console/dist/server/routes/update.js +126 -9
- package/console/dist/server/utils/pd-backups.d.ts +24 -0
- package/console/dist/server/utils/pd-backups.js +125 -0
- package/console/dist/web/assets/app.css +3 -0
- package/core/dist/runtime-v2/__tests__/architecture-regression.test.js +10 -1
- package/core/dist/runtime-v2/__tests__/architecture-regression.test.js.map +1 -1
- package/core/dist/runtime-v2/activation/__tests__/prompt-activation-reader-contract.test.js +22 -1
- package/core/dist/runtime-v2/activation/__tests__/prompt-activation-reader-contract.test.js.map +1 -1
- package/core/dist/runtime-v2/activation/prompt-activation-reader-contract.d.ts +12 -1
- package/core/dist/runtime-v2/activation/prompt-activation-reader-contract.d.ts.map +1 -1
- package/core/dist/runtime-v2/activation/prompt-activation-reader-contract.js +8 -1
- package/core/dist/runtime-v2/activation/prompt-activation-reader-contract.js.map +1 -1
- package/core/dist/runtime-v2/feature-flags/__tests__/principle-receipt-block-copy-flag.test.d.ts +2 -0
- package/core/dist/runtime-v2/feature-flags/__tests__/principle-receipt-block-copy-flag.test.d.ts.map +1 -0
- package/core/dist/runtime-v2/feature-flags/__tests__/principle-receipt-block-copy-flag.test.js +42 -0
- package/core/dist/runtime-v2/feature-flags/__tests__/principle-receipt-block-copy-flag.test.js.map +1 -0
- package/core/dist/runtime-v2/feature-flags/__tests__/principle-receipt-ledger-flag.test.d.ts +2 -0
- package/core/dist/runtime-v2/feature-flags/__tests__/principle-receipt-ledger-flag.test.d.ts.map +1 -0
- package/core/dist/runtime-v2/feature-flags/__tests__/principle-receipt-ledger-flag.test.js +42 -0
- package/core/dist/runtime-v2/feature-flags/__tests__/principle-receipt-ledger-flag.test.js.map +1 -0
- package/core/dist/runtime-v2/feature-flags/feature-flag-contract.d.ts.map +1 -1
- package/core/dist/runtime-v2/feature-flags/feature-flag-contract.js +18 -0
- package/core/dist/runtime-v2/feature-flags/feature-flag-contract.js.map +1 -1
- package/core/dist/runtime-v2/store/sqlite-connection.d.ts.map +1 -1
- package/core/dist/runtime-v2/store/sqlite-connection.js +27 -0
- package/core/dist/runtime-v2/store/sqlite-connection.js.map +1 -1
- package/dist/installer.d.ts +22 -0
- package/dist/installer.d.ts.map +1 -1
- package/dist/installer.js +67 -5
- package/dist/installer.js.map +1 -1
- package/dist/mvp-config.d.ts +8 -0
- package/dist/mvp-config.d.ts.map +1 -1
- package/dist/mvp-config.js +10 -0
- package/dist/mvp-config.js.map +1 -1
- package/dist/skill-language.d.ts +32 -0
- package/dist/skill-language.d.ts.map +1 -0
- package/dist/skill-language.js +107 -0
- package/dist/skill-language.js.map +1 -0
- package/dist/updater.d.ts.map +1 -1
- package/dist/updater.js +29 -4
- package/dist/updater.js.map +1 -1
- package/host-runtime/dist/active-principle-prompt.js +5 -1
- package/package.json +1 -1
- package/pd-cli/dist/commands/console.d.ts.map +1 -1
- package/pd-cli/dist/commands/console.js +2 -0
- package/pd-cli/dist/commands/console.js.map +1 -1
- package/pd-cli/dist/services/console-launcher.d.ts +6 -0
- package/pd-cli/dist/services/console-launcher.d.ts.map +1 -1
- package/pd-cli/dist/services/console-launcher.js.map +1 -1
- package/plugin/dist/bundle.js +550 -477
- package/plugin/dist/core/principle-application-ledger.d.ts +45 -0
- package/plugin/dist/core/principle-receipt-metadata.d.ts +15 -0
- package/plugin/dist/hooks/gate-block-helper.d.ts +4 -0
- package/plugin/dist/openclaw-sdk.d.ts +23 -1
- package/plugin/dist/openclaw.plugin.json +0 -1
- package/plugin/openclaw.plugin.json +1 -2
- package/plugin/package.json +1 -1
- package/plugin/scripts/sync-plugin.mjs +17 -8
- package/plugin/scripts/verify-build.mjs +19 -8
|
@@ -24,6 +24,7 @@ import { handleUpdateRoute } from './routes/update.js';
|
|
|
24
24
|
import { handleUpdateHistoryRoute } from './routes/update-history.js';
|
|
25
25
|
import { handleConfigRoute } from './routes/config.js';
|
|
26
26
|
import { sendJson, sendNotFound, sendUnauthorized } from './utils/response.js';
|
|
27
|
+
import { migrateLegacyExtensionBackups, resolvePdBackupsRoot } from './utils/pd-backups.js';
|
|
27
28
|
const __filename = fileURLToPath(import.meta.url);
|
|
28
29
|
const __dirname = path.dirname(__filename);
|
|
29
30
|
// WEB_ROOT calculation: server is at PKG_ROOT/dist/server/index.js
|
|
@@ -400,6 +401,21 @@ export async function main() {
|
|
|
400
401
|
console.warn('[pd-console] No auth token configured. Running without authentication. Use --token or PD_CONSOLE_TOKEN to enable.');
|
|
401
402
|
}
|
|
402
403
|
const services = await initServices(workspace, authConfig);
|
|
404
|
+
// One-time hygiene: move legacy PD backups out of ~/.openclaw/extensions so
|
|
405
|
+
// OpenClaw plugin discovery stops reporting a duplicate principles-disciple
|
|
406
|
+
// plugin on every gateway startup. Best-effort, never blocks startup.
|
|
407
|
+
try {
|
|
408
|
+
const legacy = migrateLegacyExtensionBackups();
|
|
409
|
+
if (legacy.movedFrom.length > 0) {
|
|
410
|
+
console.log(`[pd-console] Migrated ${legacy.movedFrom.length} legacy PD backup dir(s) out of the extensions dir to ${resolvePdBackupsRoot()}`);
|
|
411
|
+
}
|
|
412
|
+
for (const failure of legacy.failed) {
|
|
413
|
+
console.warn(`[pd-console] Could not migrate legacy PD backup "${failure.name}" out of the extensions dir: ${failure.reason}. Move it out manually to silence the OpenClaw "duplicate plugin id" warning.`);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
catch (err) {
|
|
417
|
+
console.warn('[pd-console] Legacy backup migration failed:', err instanceof Error ? err.message : err);
|
|
418
|
+
}
|
|
403
419
|
const server = http.createServer(handleRequest(services));
|
|
404
420
|
const shutdown = async (signal) => {
|
|
405
421
|
console.log(`[pd-console] Received ${signal}, shutting down...`);
|
|
@@ -6,6 +6,7 @@ import semver from 'semver';
|
|
|
6
6
|
import { sendSuccess, sendError, sendMethodNotAllowed, sendBadRequest, sendNotFound, } from '../utils/response.js';
|
|
7
7
|
import { appendUpdateHistory } from './update-history.js';
|
|
8
8
|
import { checkOpenClawGateway, stopOpenClawGateway, restartOpenClawGateway, } from '../utils/gateway.js';
|
|
9
|
+
import { migrateLegacyExtensionBackups, reservePdBackupDestination, resolvePdBackupsRoot, } from '../utils/pd-backups.js';
|
|
9
10
|
// ---------------------------------------------------------------------------
|
|
10
11
|
// Constants
|
|
11
12
|
// ---------------------------------------------------------------------------
|
|
@@ -82,12 +83,27 @@ function isValidMergeStrategy(value) {
|
|
|
82
83
|
function validatePathInWorkspace(target, workspaceDir) {
|
|
83
84
|
const resolved = path.resolve(target);
|
|
84
85
|
const resolvedWorkspace = path.resolve(workspaceDir);
|
|
85
|
-
// Allow paths within workspace
|
|
86
|
-
// The extensions dir
|
|
87
|
-
//
|
|
86
|
+
// Allow paths within workspace, within the OpenClaw extensions directory,
|
|
87
|
+
// or within the PD backups root. The extensions dir and backups root are
|
|
88
|
+
// under the OpenClaw install home (~/.openclaw), which is NOT derived from
|
|
89
|
+
// the workspace dir (the two may be on different drives).
|
|
88
90
|
const extensionsDir = path.resolve(resolveExtensionsDir());
|
|
89
|
-
|
|
90
|
-
|
|
91
|
+
const backupsRoot = path.resolve(resolvePdBackupsRoot());
|
|
92
|
+
const insideRoot = (root) => resolved.startsWith(root + path.sep) || resolved === root;
|
|
93
|
+
return insideRoot(resolvedWorkspace) || insideRoot(extensionsDir) || insideRoot(backupsRoot);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Log the result of a legacy-backup migration (rc-9: failures are surfaced,
|
|
97
|
+
* never silent). Returns nothing; migration itself is best-effort.
|
|
98
|
+
*/
|
|
99
|
+
function logLegacyBackupMigration(source) {
|
|
100
|
+
const legacy = migrateLegacyExtensionBackups();
|
|
101
|
+
if (legacy.movedFrom.length > 0) {
|
|
102
|
+
console.log(`[${source}] Migrated ${legacy.movedFrom.length} legacy PD backup dir(s) out of the extensions dir to ${resolvePdBackupsRoot()}`);
|
|
103
|
+
}
|
|
104
|
+
for (const failure of legacy.failed) {
|
|
105
|
+
console.warn(`[${source}] Could not migrate legacy PD backup "${failure.name}" out of the extensions dir: ${failure.reason}. Move it out manually to silence the OpenClaw "duplicate plugin id" warning.`);
|
|
106
|
+
}
|
|
91
107
|
}
|
|
92
108
|
// ---------------------------------------------------------------------------
|
|
93
109
|
// Core update operations (inline to avoid cross-package import)
|
|
@@ -128,6 +144,85 @@ function copyFileTo(src, dest) {
|
|
|
128
144
|
fs.mkdirSync(dir, { recursive: true });
|
|
129
145
|
fs.copyFileSync(src, dest);
|
|
130
146
|
}
|
|
147
|
+
// --- Plugin skill-language preservation (PR #1332 companion) -----------------
|
|
148
|
+
// OpenClaw publishes plugin skills by directory name (first declaration
|
|
149
|
+
// wins, same-name roots only warn — no locale mechanism), so a manifest must
|
|
150
|
+
// declare exactly ONE language root. The shipped manifest declares zh
|
|
151
|
+
// (product default); installs made with --lang en have their installed
|
|
152
|
+
// manifest rewritten to the en root by the installer. Updates ship a fresh
|
|
153
|
+
// zh-default manifest and would silently revert that choice — so capture the
|
|
154
|
+
// installed language BEFORE any mutation and re-apply it after the new
|
|
155
|
+
// manifest lands. Mirrors create-principles-disciple/src/skill-language.ts
|
|
156
|
+
// (keep the two transforms in sync).
|
|
157
|
+
const SKILL_LANGUAGE_ROOTS = {
|
|
158
|
+
zh: 'templates/langs/zh/skills',
|
|
159
|
+
en: 'templates/langs/en/skills',
|
|
160
|
+
};
|
|
161
|
+
function isLanguageSkillRoot(entry) {
|
|
162
|
+
if (typeof entry !== 'string')
|
|
163
|
+
return false;
|
|
164
|
+
const normalized = entry.replaceAll('\\', '/');
|
|
165
|
+
return normalized === SKILL_LANGUAGE_ROOTS.zh || normalized === SKILL_LANGUAGE_ROOTS.en;
|
|
166
|
+
}
|
|
167
|
+
function detectInstalledSkillLanguage(extDir) {
|
|
168
|
+
try {
|
|
169
|
+
const manifestPath = path.join(extDir, 'openclaw.plugin.json');
|
|
170
|
+
if (!fs.existsSync(manifestPath))
|
|
171
|
+
return 'zh';
|
|
172
|
+
const raw = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
|
|
173
|
+
if (!isRecord(raw) || !Array.isArray(raw.skills))
|
|
174
|
+
return 'zh';
|
|
175
|
+
const languages = new Set();
|
|
176
|
+
for (const entry of raw.skills) {
|
|
177
|
+
if (typeof entry !== 'string')
|
|
178
|
+
continue;
|
|
179
|
+
const normalized = entry.replaceAll('\\', '/');
|
|
180
|
+
if (normalized === SKILL_LANGUAGE_ROOTS.zh)
|
|
181
|
+
languages.add('zh');
|
|
182
|
+
else if (normalized === SKILL_LANGUAGE_ROOTS.en)
|
|
183
|
+
languages.add('en');
|
|
184
|
+
}
|
|
185
|
+
// Exactly one language root → that is the user's selection. Zero or both
|
|
186
|
+
// (pre-ERR-097 legacy install) → product default.
|
|
187
|
+
if (languages.size !== 1)
|
|
188
|
+
return 'zh';
|
|
189
|
+
for (const language of languages)
|
|
190
|
+
return language;
|
|
191
|
+
return 'zh';
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
return 'zh';
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
function reapplySkillLanguage(extDir, language) {
|
|
198
|
+
try {
|
|
199
|
+
const manifestPath = path.join(extDir, 'openclaw.plugin.json');
|
|
200
|
+
if (!fs.existsSync(manifestPath))
|
|
201
|
+
return;
|
|
202
|
+
// Defense in depth: only rewrite when the selected language's templates
|
|
203
|
+
// actually exist — otherwise the host would warn "plugin skill path not
|
|
204
|
+
// found" and publish nothing.
|
|
205
|
+
if (!fs.existsSync(path.join(extDir, SKILL_LANGUAGE_ROOTS[language]))) {
|
|
206
|
+
console.error(`[pd-console:update] skill language "${language}" templates missing — keeping shipped manifest skills`);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
const raw = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
|
|
210
|
+
if (!isRecord(raw) || !Array.isArray(raw.skills))
|
|
211
|
+
return;
|
|
212
|
+
const skills = raw.skills;
|
|
213
|
+
const kept = skills.filter((entry) => typeof entry === 'string' && !isLanguageSkillRoot(entry));
|
|
214
|
+
const next = [...kept, SKILL_LANGUAGE_ROOTS[language]];
|
|
215
|
+
const unchanged = next.length === skills.length && next.every((entry, i) => entry === skills[i]);
|
|
216
|
+
if (unchanged)
|
|
217
|
+
return;
|
|
218
|
+
raw.skills = next;
|
|
219
|
+
fs.writeFileSync(manifestPath, JSON.stringify(raw, null, 2) + '\n', 'utf-8');
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
// rc-9: degrade observably, never fail the whole update over skill language.
|
|
223
|
+
console.error(`[pd-console:update] failed to re-apply skill language "${language}": ${error instanceof Error ? error.message : String(error)}`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
131
226
|
function getAllFilesLocal(dir, skipDirs) {
|
|
132
227
|
const skip = skipDirs ?? new Set();
|
|
133
228
|
const result = [];
|
|
@@ -293,6 +388,13 @@ async function doApplyUpdate(options, workspaceDir) {
|
|
|
293
388
|
try {
|
|
294
389
|
// 0. Save current version BEFORE any changes
|
|
295
390
|
const fromVersion = readCurrentVersion(targetDir) ?? 'unknown';
|
|
391
|
+
// Capture the installed skill language before the tarball's manifest
|
|
392
|
+
// overwrites it (PR #1332 companion — see reapplySkillLanguage).
|
|
393
|
+
const skillLanguage = detectInstalledSkillLanguage(targetDir);
|
|
394
|
+
// 0.5 Heal legacy installs: older versions left backups inside the
|
|
395
|
+
// extensions dir, where OpenClaw discovery picks them up as duplicate
|
|
396
|
+
// plugins. Move them out before creating a new backup alongside them.
|
|
397
|
+
logLegacyBackupMigration('pd-update');
|
|
296
398
|
// 1. Fetch latest package info (with timeout + retry)
|
|
297
399
|
const response = await fetchWithRetry(NPM_REGISTRY_LATEST, 'Registry check');
|
|
298
400
|
const rawData = await response.json();
|
|
@@ -307,10 +409,13 @@ async function doApplyUpdate(options, workspaceDir) {
|
|
|
307
409
|
if (!tarball)
|
|
308
410
|
return { success: false, message: 'Missing tarball URL in registry response' };
|
|
309
411
|
// 2. Create backup if requested (Fix 2: skip node_modules to avoid EPERM
|
|
310
|
-
// from locked native modules and npm symlinks/junctions)
|
|
412
|
+
// from locked native modules and npm symlinks/junctions).
|
|
413
|
+
// The backup lives in <openclawHome>/pd-backups — OUTSIDE the
|
|
414
|
+
// extensions dir, because OpenClaw plugin discovery scans every
|
|
415
|
+
// extensions/ child and would report the backup as a duplicate
|
|
416
|
+
// "principles-disciple" plugin on every gateway startup.
|
|
311
417
|
if (createBackup) {
|
|
312
|
-
|
|
313
|
-
backupPath = path.join(path.dirname(targetDir), `.pd-backup-${timestamp}`);
|
|
418
|
+
backupPath = reservePdBackupDestination(path.basename(targetDir));
|
|
314
419
|
copyDirRecursive(targetDir, backupPath, SKIP_DIRS);
|
|
315
420
|
}
|
|
316
421
|
// 3. Download and extract new version (with timeout + retry)
|
|
@@ -355,6 +460,9 @@ async function doApplyUpdate(options, workspaceDir) {
|
|
|
355
460
|
copyFileTo(path.join(tempDir, file), path.join(targetDir, file));
|
|
356
461
|
updatedFiles.push(file);
|
|
357
462
|
}
|
|
463
|
+
// 4b. The incoming manifest declares the product-default skill language
|
|
464
|
+
// (zh); restore the language this install was materialized with.
|
|
465
|
+
reapplySkillLanguage(targetDir, skillLanguage);
|
|
358
466
|
// Fix 3: deletions intentionally skipped — see comment above.
|
|
359
467
|
// 5. Update package.json version
|
|
360
468
|
const pkgPath = path.join(targetDir, 'package.json');
|
|
@@ -487,7 +595,13 @@ async function doInlineFullUpdate(workspaceDir) {
|
|
|
487
595
|
}
|
|
488
596
|
// Capture version before changes
|
|
489
597
|
const fromVersion = readCurrentVersion(extDir) ?? 'unknown';
|
|
598
|
+
// Capture the installed skill language before the tarball's manifest
|
|
599
|
+
// overwrites it (PR #1332 companion — see reapplySkillLanguage).
|
|
600
|
+
const skillLanguage = detectInstalledSkillLanguage(extDir);
|
|
490
601
|
let tempDir;
|
|
602
|
+
// Heal legacy installs: move old backups out of the extensions dir so
|
|
603
|
+
// OpenClaw discovery stops reporting a duplicate plugin (see pd-backups.ts).
|
|
604
|
+
logLegacyBackupMigration('pd-update-full');
|
|
491
605
|
try {
|
|
492
606
|
// 2. Fetch installer package info from npm
|
|
493
607
|
const response = await fetchWithRetry(NPM_REGISTRY_INSTALLER, 'Installer registry check');
|
|
@@ -527,6 +641,9 @@ async function doInlineFullUpdate(workspaceDir) {
|
|
|
527
641
|
const pluginSrc = path.join(tempDir, 'plugin');
|
|
528
642
|
if (fs.existsSync(pluginSrc)) {
|
|
529
643
|
copyDirRecursive(pluginSrc, extDir, SKIP_DIRS);
|
|
644
|
+
// The incoming manifest declares the product-default skill language
|
|
645
|
+
// (zh); restore the language this install was materialized with.
|
|
646
|
+
reapplySkillLanguage(extDir, skillLanguage);
|
|
530
647
|
}
|
|
531
648
|
// b. console/ → extDir/console/ (overwrite dist/ files; do NOT rmSync —
|
|
532
649
|
// console/node_modules/ may contain locked native modules like
|
|
@@ -725,7 +842,7 @@ export async function handleUpdateRoute(req, res, workspaceDir, subPath) {
|
|
|
725
842
|
return;
|
|
726
843
|
}
|
|
727
844
|
if (!validatePathInWorkspace(backupDir, workspaceDir)) {
|
|
728
|
-
sendBadRequest(res, 'backupDir must be within workspace or
|
|
845
|
+
sendBadRequest(res, 'backupDir must be within the workspace, extensions directory, or PD backups directory');
|
|
729
846
|
return;
|
|
730
847
|
}
|
|
731
848
|
const result = await doRollbackUpdate({ targetDir, backupDir }, workspaceDir);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface LegacyBackupMigrationResult {
|
|
2
|
+
movedFrom: string[];
|
|
3
|
+
failed: {
|
|
4
|
+
name: string;
|
|
5
|
+
reason: string;
|
|
6
|
+
}[];
|
|
7
|
+
}
|
|
8
|
+
/** Backups root — outside the extensions dir OpenClaw scans for plugins. */
|
|
9
|
+
export declare function resolvePdBackupsRoot(): string;
|
|
10
|
+
/**
|
|
11
|
+
* Reserve a unique backup destination under the backups root:
|
|
12
|
+
* <root>/<targetName>-<ISO timestamp>, with a numeric suffix on collision.
|
|
13
|
+
* Atomic reservation via plain (non-recursive) mkdirSync — EEXIST means the
|
|
14
|
+
* name is taken, retry with the next suffix. The retry loop is bounded; never
|
|
15
|
+
* poll existsSync in a loop (a stale/mocked always-true predicate makes it
|
|
16
|
+
* unbounded).
|
|
17
|
+
*/
|
|
18
|
+
export declare function reservePdBackupDestination(targetName: string): string;
|
|
19
|
+
/**
|
|
20
|
+
* Move legacy PD backup directories out of the extensions dir into the
|
|
21
|
+
* backups root, so OpenClaw stops discovering them as duplicate plugins.
|
|
22
|
+
* Best-effort per entry (rc-9: failures are reported, never silent).
|
|
23
|
+
*/
|
|
24
|
+
export declare function migrateLegacyExtensionBackups(): LegacyBackupMigrationResult;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PD plugin backup location utilities.
|
|
3
|
+
*
|
|
4
|
+
* Backups must NEVER live inside ~/.openclaw/extensions: OpenClaw's plugin
|
|
5
|
+
* discovery scans every child directory of the extensions root, so a backup
|
|
6
|
+
* containing package.json (openclaw.extensions) + dist/bundle.js is discovered
|
|
7
|
+
* as a SECOND "principles-disciple" plugin, producing
|
|
8
|
+
* "duplicate plugin id detected; global plugin will be overridden by global
|
|
9
|
+
* plugin" on every gateway startup.
|
|
10
|
+
*
|
|
11
|
+
* Backups therefore live in <openclawHome>/pd-backups, which is outside every
|
|
12
|
+
* plugin discovery root. Legacy backups that older PD versions left inside
|
|
13
|
+
* extensions/ (".pd-backup-<ts>" from the console updater,
|
|
14
|
+
* "principles-disciple.backup.<ms>" from the installer) are migrated out by
|
|
15
|
+
* migrateLegacyExtensionBackups().
|
|
16
|
+
*/
|
|
17
|
+
import * as fs from 'fs';
|
|
18
|
+
import * as path from 'path';
|
|
19
|
+
import * as os from 'os';
|
|
20
|
+
/**
|
|
21
|
+
* Resolve the OpenClaw install home.
|
|
22
|
+
* Priority: OPENCLAW_HOME env var (explicit override) > ~/.openclaw
|
|
23
|
+
*/
|
|
24
|
+
function resolveOpenClawHome() {
|
|
25
|
+
const envHome = process.env.OPENCLAW_HOME;
|
|
26
|
+
if (envHome && envHome.trim().length > 0)
|
|
27
|
+
return path.resolve(envHome);
|
|
28
|
+
return path.join(os.homedir(), '.openclaw');
|
|
29
|
+
}
|
|
30
|
+
/** Backups root — outside the extensions dir OpenClaw scans for plugins. */
|
|
31
|
+
export function resolvePdBackupsRoot() {
|
|
32
|
+
return path.join(resolveOpenClawHome(), 'pd-backups');
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Extract a POSIX errno code from a thrown fs error (rc-2: type guard, no
|
|
36
|
+
* blind `as` cast on the error object).
|
|
37
|
+
*/
|
|
38
|
+
function getErrorCode(err) {
|
|
39
|
+
if (err !== null && typeof err === 'object' && Object.hasOwn(err, 'code')) {
|
|
40
|
+
const { code } = err;
|
|
41
|
+
return typeof code === 'string' ? code : undefined;
|
|
42
|
+
}
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Reserve a unique backup destination under the backups root:
|
|
47
|
+
* <root>/<targetName>-<ISO timestamp>, with a numeric suffix on collision.
|
|
48
|
+
* Atomic reservation via plain (non-recursive) mkdirSync — EEXIST means the
|
|
49
|
+
* name is taken, retry with the next suffix. The retry loop is bounded; never
|
|
50
|
+
* poll existsSync in a loop (a stale/mocked always-true predicate makes it
|
|
51
|
+
* unbounded).
|
|
52
|
+
*/
|
|
53
|
+
export function reservePdBackupDestination(targetName) {
|
|
54
|
+
const root = resolvePdBackupsRoot();
|
|
55
|
+
fs.mkdirSync(root, { recursive: true });
|
|
56
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
57
|
+
let lastError;
|
|
58
|
+
for (let attempt = 0; attempt < 100; attempt++) {
|
|
59
|
+
const suffix = attempt === 0 ? '' : `-${attempt}`;
|
|
60
|
+
const candidate = path.join(root, `${targetName}-${timestamp}${suffix}`);
|
|
61
|
+
try {
|
|
62
|
+
fs.mkdirSync(candidate);
|
|
63
|
+
return candidate;
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
if (getErrorCode(err) === 'EEXIST') {
|
|
67
|
+
lastError = err;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
throw err;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
throw new Error(`Could not reserve a unique backup directory under ${root}: ${lastError instanceof Error ? lastError.message : String(lastError)}`);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Backup directory names older PD versions created as siblings of the plugin
|
|
77
|
+
* inside the extensions dir (both are re-discovered as duplicate plugins).
|
|
78
|
+
*/
|
|
79
|
+
function isLegacyPdBackupName(name) {
|
|
80
|
+
// Console updater (<= 2026-08): ".pd-backup-<ISO ts>" sibling.
|
|
81
|
+
if (name.startsWith('.pd-backup-'))
|
|
82
|
+
return true;
|
|
83
|
+
// Installer (<= 2026-08): "<extDir>.backup.<epoch ms>" sibling.
|
|
84
|
+
return /^principles-disciple\.backup\.\d+$/.test(name);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Move legacy PD backup directories out of the extensions dir into the
|
|
88
|
+
* backups root, so OpenClaw stops discovering them as duplicate plugins.
|
|
89
|
+
* Best-effort per entry (rc-9: failures are reported, never silent).
|
|
90
|
+
*/
|
|
91
|
+
export function migrateLegacyExtensionBackups() {
|
|
92
|
+
const result = { movedFrom: [], failed: [] };
|
|
93
|
+
const extensionsDir = path.join(resolveOpenClawHome(), 'extensions');
|
|
94
|
+
let entries;
|
|
95
|
+
try {
|
|
96
|
+
entries = fs.readdirSync(extensionsDir, { withFileTypes: true });
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return result; // no extensions dir — nothing to migrate
|
|
100
|
+
}
|
|
101
|
+
const root = resolvePdBackupsRoot();
|
|
102
|
+
for (const entry of entries) {
|
|
103
|
+
if (!entry.isDirectory() || !isLegacyPdBackupName(entry.name))
|
|
104
|
+
continue;
|
|
105
|
+
const from = path.join(extensionsDir, entry.name);
|
|
106
|
+
let to = path.join(root, entry.name);
|
|
107
|
+
let suffix = 1;
|
|
108
|
+
while (fs.existsSync(to)) {
|
|
109
|
+
to = path.join(root, `${entry.name}-${suffix}`);
|
|
110
|
+
suffix += 1;
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
fs.mkdirSync(root, { recursive: true });
|
|
114
|
+
fs.renameSync(from, to);
|
|
115
|
+
result.movedFrom.push(from);
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
result.failed.push({
|
|
119
|
+
name: entry.name,
|
|
120
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return result;
|
|
125
|
+
}
|
|
@@ -328,6 +328,12 @@ const KNOWN_PLUGIN_CORE_FILES = new Set([
|
|
|
328
328
|
// I/O shell: reads .pd/config.yaml, constructs PiAiRuntimeAdapter, writes trajectory.
|
|
329
329
|
// Pure detection logic lives in @principles/core/runtime-v2/signal-collector/.
|
|
330
330
|
'signal-collector-host.ts',
|
|
331
|
+
// PRI-530: Plugin I/O boundary — sync readonly state.db reads resolving principle
|
|
332
|
+
// receipt attribution (title fallback chain / approval date / source summary).
|
|
333
|
+
'principle-receipt-metadata.ts',
|
|
334
|
+
// PRI-531: Plugin I/O boundary — durable receipt ledger writes (state.db
|
|
335
|
+
// principle_applications inserts + 90-day retention sweep).
|
|
336
|
+
'principle-application-ledger.ts',
|
|
331
337
|
'pain-lifecycle.ts',
|
|
332
338
|
'session-tracker.ts',
|
|
333
339
|
// PRI-459: now a thin re-export adapter over @principles/core/principle-tree-ledger.
|
|
@@ -429,7 +435,10 @@ describe('PRI-212 plugin core anti-growth guard', () => {
|
|
|
429
435
|
// collection I/O host (correction + empathy upstream merge). Reads
|
|
430
436
|
// .pd/config.yaml, constructs PiAiRuntimeAdapter, writes trajectory. Pure
|
|
431
437
|
// detection logic lives in @principles/core/runtime-v2/signal-collector/.
|
|
432
|
-
|
|
438
|
+
// PRI-530: Added principle-receipt-metadata.ts (94 → 95) — plugin I/O boundary
|
|
439
|
+
// for principle receipt attribution reads (readonly state.db joins).
|
|
440
|
+
// PRI-531: Added principle-application-ledger.ts (95 → 96) — receipt ledger writer.
|
|
441
|
+
expect(KNOWN_PLUGIN_CORE_FILES.size).toBe(96);
|
|
433
442
|
});
|
|
434
443
|
});
|
|
435
444
|
const REQUIRED_TEST_FILES = [
|