@jaimevalasek/aioson 1.20.0 → 1.21.3
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/CHANGELOG.md +38 -0
- package/docs/pt/living-memory/reflexao-in-harness.md +2 -0
- package/package.json +1 -1
- package/src/cli.js +5 -0
- package/src/commands/context-health.js +35 -2
- package/src/commands/feature-close.js +36 -0
- package/src/commands/install.js +5 -0
- package/src/commands/memory-archive.js +193 -193
- package/src/commands/memory-reflect-commit.js +28 -4
- package/src/commands/memory-restore.js +177 -177
- package/src/commands/memory-search.js +135 -135
- package/src/commands/memory-trim.js +191 -0
- package/src/constants.js +1 -0
- package/src/current-state-trim.js +170 -0
- package/src/doctor.js +17 -0
- package/src/i18n/messages/en.js +16 -0
- package/src/i18n/messages/es.js +16 -0
- package/src/i18n/messages/fr.js +16 -0
- package/src/i18n/messages/pt-BR.js +16 -0
- package/src/install-wizard.js +3 -2
- package/src/lib/tool-capabilities.js +67 -64
- package/src/memory-reflect-engine.js +10 -4
- package/src/permissions-generator.js +3 -0
- package/template/.aioson/agents/architect.md +3 -0
- package/template/.aioson/agents/committer.md +1 -1
- package/template/.aioson/agents/dev.md +2 -2
- package/template/.aioson/agents/deyvin.md +2 -1
- package/template/.aioson/agents/pentester.md +1 -0
- package/template/.aioson/agents/qa.md +4 -0
- package/template/.aioson/agents/sheldon.md +1 -0
- package/template/.aioson/agents/tester.md +2 -0
- package/template/.aioson/config/autonomy-protocol.json +1 -0
- package/template/.aioson/design-docs/agent-loading-contract.md +138 -0
- package/template/.aioson/docs/quality/code-health-analysis.md +79 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
// aioson memory:reflect-commit [.] --agent=<name> [--output=<path-to-json>] [--json]
|
|
3
|
+
// aioson memory:reflect-commit [.] --agent=<name> [--output=<path-to-json>] [--json] [--dry-run]
|
|
4
4
|
//
|
|
5
5
|
// Reads the reflect manifest written by memory:reflect-prepare, accepts the
|
|
6
6
|
// agent's reflected output (as a JSON map of relative path → new content),
|
|
@@ -63,13 +63,14 @@ async function emitEvent(targetDir, agent, type, message, payload) {
|
|
|
63
63
|
async function runMemoryReflectCommit({ args, options = {}, logger }) {
|
|
64
64
|
const targetDir = path.resolve(process.cwd(), args?.[0] || '.');
|
|
65
65
|
const agent = String(options.agent || '').trim() || 'unknown';
|
|
66
|
+
const dryRun = Boolean(options['dry-run'] || options.dryRun);
|
|
66
67
|
|
|
67
68
|
const manifestPath = path.join(targetDir, REFLECT_PROMPT_RELATIVE);
|
|
68
69
|
let manifest;
|
|
69
70
|
try {
|
|
70
71
|
manifest = await readJsonFile(manifestPath);
|
|
71
72
|
} catch {
|
|
72
|
-
const message = `manifest not found at ${path.relative(targetDir, manifestPath)} — run memory:reflect-prepare
|
|
73
|
+
const message = `manifest not found at ${path.relative(targetDir, manifestPath)} — it may have been consumed by a previous successful reflect-commit; run memory:reflect-prepare to generate a new one`;
|
|
73
74
|
if (!options.json) logger.log(`✗ ${message}`);
|
|
74
75
|
return { ok: false, error: 'missing_manifest', message };
|
|
75
76
|
}
|
|
@@ -102,9 +103,11 @@ async function runMemoryReflectCommit({ args, options = {}, logger }) {
|
|
|
102
103
|
// verify that every resolved absolute path stays under the project's
|
|
103
104
|
// bootstrap directory. validate() already rejects absolute paths and
|
|
104
105
|
// `..` segments, but this is the second wall in case the manifest's
|
|
105
|
-
// allowed_paths is ever extended beyond bootstrap/.
|
|
106
|
+
// allowed_paths is ever extended beyond bootstrap/. Run this for BOTH
|
|
107
|
+
// dry-run and real commits so a dry-run validates exactly what a real
|
|
108
|
+
// commit would do.
|
|
106
109
|
const bootstrapRoot = path.resolve(targetDir, '.aioson/context/bootstrap');
|
|
107
|
-
const
|
|
110
|
+
const planned = [];
|
|
108
111
|
for (const [relPath, content] of Object.entries(files)) {
|
|
109
112
|
const absPath = path.resolve(targetDir, relPath);
|
|
110
113
|
if (!absPath.startsWith(bootstrapRoot + path.sep) && absPath !== bootstrapRoot) {
|
|
@@ -116,6 +119,27 @@ async function runMemoryReflectCommit({ args, options = {}, logger }) {
|
|
|
116
119
|
});
|
|
117
120
|
return { ok: false, error: 'path_escape', message: msg };
|
|
118
121
|
}
|
|
122
|
+
planned.push({ relPath, absPath, content });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// --dry-run: validation + path containment already passed. Report what WOULD
|
|
126
|
+
// be written and stop — no disk writes, and crucially the manifest is NOT
|
|
127
|
+
// consumed, so a real commit can still follow. A dry-run must never mutate
|
|
128
|
+
// state (this is the bug fix: previously --dry-run was ignored and ran the
|
|
129
|
+
// full destructive commit).
|
|
130
|
+
if (dryRun) {
|
|
131
|
+
const wouldWrite = planned.map((p) => p.relPath);
|
|
132
|
+
if (!options.json) {
|
|
133
|
+
logger.log('• reflect-commit DRY RUN — no files written, manifest preserved');
|
|
134
|
+
for (const w of wouldWrite) logger.log(` - would write: ${w}`);
|
|
135
|
+
}
|
|
136
|
+
await emitEvent(targetDir, agent, 'memory_reflect_dry_run',
|
|
137
|
+
`dry-run validated ${wouldWrite.length} file(s)`, { would_write: wouldWrite });
|
|
138
|
+
return { ok: true, dryRun: true, would_write: wouldWrite };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const written = [];
|
|
142
|
+
for (const { relPath, absPath, content } of planned) {
|
|
119
143
|
// SF-project-22: scrub zero-width / bidi / HTML-comment injection carriers
|
|
120
144
|
// from the LLM-authored content before it lands in bootstrap. Path
|
|
121
145
|
// containment is already enforced above; this is the content layer of the
|
|
@@ -1,177 +1,177 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* aioson memory:restore --id=<rule|learning|brain>:<slug> [--reason="<text>"] [--dry-run] [--json]
|
|
5
|
-
*
|
|
6
|
-
* Tier-2 human-actioned command. Moves an archived artifact back to its
|
|
7
|
-
* active path and records an `event_type='restored'` entry with a fresh
|
|
8
|
-
* `start_at` (PMD-10). History is preserved: the prior `archived` entry
|
|
9
|
-
* keeps its `end_at`.
|
|
10
|
-
*
|
|
11
|
-
* Refuses to run when `process.env.AIOSON_RUNTIME_HOOK === '1'` (BR-ALL-01).
|
|
12
|
-
* Emits `aioson notify --level=warn` BEFORE mutation (BR-ALL-06).
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
const path = require('node:path');
|
|
16
|
-
const { openRuntimeDb } = require('../runtime-store');
|
|
17
|
-
const { runNotify } = require('./notify');
|
|
18
|
-
const {
|
|
19
|
-
parseTargetId,
|
|
20
|
-
normalizeKind,
|
|
21
|
-
restoreTarget,
|
|
22
|
-
TARGET_TYPES
|
|
23
|
-
} = require('../learning-loop-archive');
|
|
24
|
-
|
|
25
|
-
function tFn(t, key, params) {
|
|
26
|
-
if (typeof t === 'function') {
|
|
27
|
-
try { return t(key, params || {}); } catch { /* fall through */ }
|
|
28
|
-
}
|
|
29
|
-
return null;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function isHookContext() {
|
|
33
|
-
return process.env.AIOSON_RUNTIME_HOOK === '1';
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
async function runMemoryRestore({ args, options = {}, logger, t }) {
|
|
37
|
-
const targetDir = path.resolve(process.cwd(), args && args[0] ? args[0] : '.');
|
|
38
|
-
const wantJson = Boolean(options.json);
|
|
39
|
-
const dryRun = Boolean(options['dry-run'] || options.dryRun);
|
|
40
|
-
const log = (msg) => { if (logger && typeof logger.log === 'function') logger.log(msg); };
|
|
41
|
-
|
|
42
|
-
if (isHookContext()) {
|
|
43
|
-
const msg = tFn(t, 'memory_restore.hook_blocked')
|
|
44
|
-
|| 'memory:restore cannot be invoked from a runtime hook (BR-ALL-01: tier-2 requires human action).';
|
|
45
|
-
if (wantJson) return { ok: false, reason: 'hook_blocked' };
|
|
46
|
-
log(msg);
|
|
47
|
-
return { ok: false, reason: 'hook_blocked' };
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const rawId = options.id || options.target || '';
|
|
51
|
-
const reason = options.reason ? String(options.reason).trim() : '';
|
|
52
|
-
|
|
53
|
-
if (!rawId) {
|
|
54
|
-
const msg = tFn(t, 'memory_restore.id_required')
|
|
55
|
-
|| 'memory:restore requires --id=<rule|learning|brain>:<slug>.';
|
|
56
|
-
if (wantJson) return { ok: false, reason: 'missing_id' };
|
|
57
|
-
log(msg);
|
|
58
|
-
return { ok: false, reason: 'missing_id' };
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const parsed = parseTargetId(rawId);
|
|
62
|
-
const kind = normalizeKind(parsed.kind);
|
|
63
|
-
if (!kind || !TARGET_TYPES.has(kind) || !parsed.slug) {
|
|
64
|
-
const msg = tFn(t, 'memory_restore.invalid_id', { value: rawId })
|
|
65
|
-
|| `memory:restore invalid --id value: "${rawId}". Expected rule|learning|brain:<slug>.`;
|
|
66
|
-
if (wantJson) return { ok: false, reason: 'invalid_id', value: rawId };
|
|
67
|
-
log(msg);
|
|
68
|
-
return { ok: false, reason: 'invalid_id' };
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const notifyMessage = tFn(t, 'memory_restore.notify_template', {
|
|
72
|
-
kind,
|
|
73
|
-
slug: parsed.slug,
|
|
74
|
-
reason: reason || 'restoring archived artifact'
|
|
75
|
-
}) || `restoring ${kind} "${parsed.slug}"${reason ? `: ${reason}` : ''}`;
|
|
76
|
-
let notifyResult;
|
|
77
|
-
try {
|
|
78
|
-
notifyResult = await runNotify({
|
|
79
|
-
args: [targetDir],
|
|
80
|
-
options: {
|
|
81
|
-
level: 'warn',
|
|
82
|
-
topic: 'memory',
|
|
83
|
-
message: notifyMessage,
|
|
84
|
-
agent: 'memory-restore',
|
|
85
|
-
json: wantJson ? true : undefined
|
|
86
|
-
},
|
|
87
|
-
logger: logger || { log: () => {} }
|
|
88
|
-
});
|
|
89
|
-
} catch (err) {
|
|
90
|
-
if (wantJson) return { ok: false, reason: 'notify_failed', error: String(err && err.message || err) };
|
|
91
|
-
log(`memory:restore notify failed: ${err && err.message ? err.message : err}`);
|
|
92
|
-
return { ok: false, reason: 'notify_failed' };
|
|
93
|
-
}
|
|
94
|
-
if (notifyResult && notifyResult.ok === false) {
|
|
95
|
-
if (wantJson) return { ok: false, reason: 'notify_blocked', exitCode: notifyResult.exitCode };
|
|
96
|
-
log('memory:restore aborted: tier-2 notify returned non-zero exit code.');
|
|
97
|
-
return { ok: false, reason: 'notify_blocked' };
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
let dbHandle;
|
|
101
|
-
try {
|
|
102
|
-
dbHandle = await openRuntimeDb(targetDir);
|
|
103
|
-
} catch (err) {
|
|
104
|
-
if (wantJson) return { ok: false, reason: 'runtime_db_unavailable', error: String(err && err.message || err) };
|
|
105
|
-
log(`memory:restore runtime db unavailable: ${err && err.message ? err.message : err}`);
|
|
106
|
-
return { ok: false, reason: 'runtime_db_unavailable' };
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
const { db } = dbHandle;
|
|
110
|
-
let outcome;
|
|
111
|
-
try {
|
|
112
|
-
outcome = restoreTarget(db, {
|
|
113
|
-
targetDir,
|
|
114
|
-
kind,
|
|
115
|
-
slug: parsed.slug,
|
|
116
|
-
reason: reason || null,
|
|
117
|
-
actor: 'human',
|
|
118
|
-
featureSlug: options.feature ? String(options.feature).trim() : null,
|
|
119
|
-
dryRun
|
|
120
|
-
});
|
|
121
|
-
} finally {
|
|
122
|
-
db.close();
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
if (!outcome.ok) {
|
|
126
|
-
if (wantJson) return outcome;
|
|
127
|
-
if (outcome.reason === 'target_not_archived') {
|
|
128
|
-
const msg = tFn(t, 'memory_restore.target_not_archived', { kind, slug: parsed.slug })
|
|
129
|
-
|| `memory:restore: ${kind} "${parsed.slug}" not found in archive.`;
|
|
130
|
-
log(msg);
|
|
131
|
-
} else if (outcome.reason === 'target_already_active') {
|
|
132
|
-
const msg = tFn(t, 'memory_restore.target_already_active', { kind, slug: parsed.slug })
|
|
133
|
-
|| `memory:restore: ${kind} "${parsed.slug}" already active. No-op.`;
|
|
134
|
-
log(msg);
|
|
135
|
-
} else if (outcome.reason === 'target_not_found') {
|
|
136
|
-
const msg = tFn(t, 'memory_restore.target_not_found', { kind, slug: parsed.slug })
|
|
137
|
-
|| `memory:restore: ${kind} "${parsed.slug}" not found.`;
|
|
138
|
-
log(msg);
|
|
139
|
-
} else {
|
|
140
|
-
log(`memory:restore failed: ${outcome.reason}${outcome.error ? ' — ' + outcome.error : ''}`);
|
|
141
|
-
}
|
|
142
|
-
return outcome;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
if (wantJson) {
|
|
146
|
-
return {
|
|
147
|
-
ok: true,
|
|
148
|
-
dry_run: Boolean(outcome.dryRun),
|
|
149
|
-
kind,
|
|
150
|
-
slug: parsed.slug,
|
|
151
|
-
source_path: outcome.sourcePath,
|
|
152
|
-
dest_path: outcome.destPath,
|
|
153
|
-
restored_entry_id: outcome.restoredEntryId || null,
|
|
154
|
-
start_at: outcome.startAt || null
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
if (outcome.dryRun) {
|
|
159
|
-
const msg = tFn(t, 'memory_restore.dry_run_summary', {
|
|
160
|
-
kind,
|
|
161
|
-
slug: parsed.slug,
|
|
162
|
-
source: outcome.sourcePath || kind,
|
|
163
|
-
dest: outcome.destPath || '(no dest)'
|
|
164
|
-
}) || `memory:restore [dry-run]: would move ${outcome.sourcePath || kind} → ${outcome.destPath || '(no dest)'}.`;
|
|
165
|
-
log(msg);
|
|
166
|
-
} else {
|
|
167
|
-
const msg = tFn(t, 'memory_restore.restored_success', {
|
|
168
|
-
kind,
|
|
169
|
-
slug: parsed.slug,
|
|
170
|
-
dest: outcome.destPath || ''
|
|
171
|
-
}) || `memory:restore ✓ ${kind} "${parsed.slug}" restored${outcome.destPath ? ' to ' + outcome.destPath : ''}.`;
|
|
172
|
-
log(msg);
|
|
173
|
-
}
|
|
174
|
-
return outcome;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
module.exports = { runMemoryRestore };
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* aioson memory:restore --id=<rule|learning|brain>:<slug> [--reason="<text>"] [--dry-run] [--json]
|
|
5
|
+
*
|
|
6
|
+
* Tier-2 human-actioned command. Moves an archived artifact back to its
|
|
7
|
+
* active path and records an `event_type='restored'` entry with a fresh
|
|
8
|
+
* `start_at` (PMD-10). History is preserved: the prior `archived` entry
|
|
9
|
+
* keeps its `end_at`.
|
|
10
|
+
*
|
|
11
|
+
* Refuses to run when `process.env.AIOSON_RUNTIME_HOOK === '1'` (BR-ALL-01).
|
|
12
|
+
* Emits `aioson notify --level=warn` BEFORE mutation (BR-ALL-06).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const path = require('node:path');
|
|
16
|
+
const { openRuntimeDb } = require('../runtime-store');
|
|
17
|
+
const { runNotify } = require('./notify');
|
|
18
|
+
const {
|
|
19
|
+
parseTargetId,
|
|
20
|
+
normalizeKind,
|
|
21
|
+
restoreTarget,
|
|
22
|
+
TARGET_TYPES
|
|
23
|
+
} = require('../learning-loop-archive');
|
|
24
|
+
|
|
25
|
+
function tFn(t, key, params) {
|
|
26
|
+
if (typeof t === 'function') {
|
|
27
|
+
try { return t(key, params || {}); } catch { /* fall through */ }
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function isHookContext() {
|
|
33
|
+
return process.env.AIOSON_RUNTIME_HOOK === '1';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function runMemoryRestore({ args, options = {}, logger, t }) {
|
|
37
|
+
const targetDir = path.resolve(process.cwd(), args && args[0] ? args[0] : '.');
|
|
38
|
+
const wantJson = Boolean(options.json);
|
|
39
|
+
const dryRun = Boolean(options['dry-run'] || options.dryRun);
|
|
40
|
+
const log = (msg) => { if (logger && typeof logger.log === 'function') logger.log(msg); };
|
|
41
|
+
|
|
42
|
+
if (isHookContext()) {
|
|
43
|
+
const msg = tFn(t, 'cli.memory_restore.hook_blocked')
|
|
44
|
+
|| 'memory:restore cannot be invoked from a runtime hook (BR-ALL-01: tier-2 requires human action).';
|
|
45
|
+
if (wantJson) return { ok: false, reason: 'hook_blocked' };
|
|
46
|
+
log(msg);
|
|
47
|
+
return { ok: false, reason: 'hook_blocked' };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const rawId = options.id || options.target || '';
|
|
51
|
+
const reason = options.reason ? String(options.reason).trim() : '';
|
|
52
|
+
|
|
53
|
+
if (!rawId) {
|
|
54
|
+
const msg = tFn(t, 'cli.memory_restore.id_required')
|
|
55
|
+
|| 'memory:restore requires --id=<rule|learning|brain>:<slug>.';
|
|
56
|
+
if (wantJson) return { ok: false, reason: 'missing_id' };
|
|
57
|
+
log(msg);
|
|
58
|
+
return { ok: false, reason: 'missing_id' };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const parsed = parseTargetId(rawId);
|
|
62
|
+
const kind = normalizeKind(parsed.kind);
|
|
63
|
+
if (!kind || !TARGET_TYPES.has(kind) || !parsed.slug) {
|
|
64
|
+
const msg = tFn(t, 'cli.memory_restore.invalid_id', { value: rawId })
|
|
65
|
+
|| `memory:restore invalid --id value: "${rawId}". Expected rule|learning|brain:<slug>.`;
|
|
66
|
+
if (wantJson) return { ok: false, reason: 'invalid_id', value: rawId };
|
|
67
|
+
log(msg);
|
|
68
|
+
return { ok: false, reason: 'invalid_id' };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const notifyMessage = tFn(t, 'cli.memory_restore.notify_template', {
|
|
72
|
+
kind,
|
|
73
|
+
slug: parsed.slug,
|
|
74
|
+
reason: reason || 'restoring archived artifact'
|
|
75
|
+
}) || `restoring ${kind} "${parsed.slug}"${reason ? `: ${reason}` : ''}`;
|
|
76
|
+
let notifyResult;
|
|
77
|
+
try {
|
|
78
|
+
notifyResult = await runNotify({
|
|
79
|
+
args: [targetDir],
|
|
80
|
+
options: {
|
|
81
|
+
level: 'warn',
|
|
82
|
+
topic: 'memory',
|
|
83
|
+
message: notifyMessage,
|
|
84
|
+
agent: 'memory-restore',
|
|
85
|
+
json: wantJson ? true : undefined
|
|
86
|
+
},
|
|
87
|
+
logger: logger || { log: () => {} }
|
|
88
|
+
});
|
|
89
|
+
} catch (err) {
|
|
90
|
+
if (wantJson) return { ok: false, reason: 'notify_failed', error: String(err && err.message || err) };
|
|
91
|
+
log(`memory:restore notify failed: ${err && err.message ? err.message : err}`);
|
|
92
|
+
return { ok: false, reason: 'notify_failed' };
|
|
93
|
+
}
|
|
94
|
+
if (notifyResult && notifyResult.ok === false) {
|
|
95
|
+
if (wantJson) return { ok: false, reason: 'notify_blocked', exitCode: notifyResult.exitCode };
|
|
96
|
+
log('memory:restore aborted: tier-2 notify returned non-zero exit code.');
|
|
97
|
+
return { ok: false, reason: 'notify_blocked' };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
let dbHandle;
|
|
101
|
+
try {
|
|
102
|
+
dbHandle = await openRuntimeDb(targetDir);
|
|
103
|
+
} catch (err) {
|
|
104
|
+
if (wantJson) return { ok: false, reason: 'runtime_db_unavailable', error: String(err && err.message || err) };
|
|
105
|
+
log(`memory:restore runtime db unavailable: ${err && err.message ? err.message : err}`);
|
|
106
|
+
return { ok: false, reason: 'runtime_db_unavailable' };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const { db } = dbHandle;
|
|
110
|
+
let outcome;
|
|
111
|
+
try {
|
|
112
|
+
outcome = restoreTarget(db, {
|
|
113
|
+
targetDir,
|
|
114
|
+
kind,
|
|
115
|
+
slug: parsed.slug,
|
|
116
|
+
reason: reason || null,
|
|
117
|
+
actor: 'human',
|
|
118
|
+
featureSlug: options.feature ? String(options.feature).trim() : null,
|
|
119
|
+
dryRun
|
|
120
|
+
});
|
|
121
|
+
} finally {
|
|
122
|
+
db.close();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (!outcome.ok) {
|
|
126
|
+
if (wantJson) return outcome;
|
|
127
|
+
if (outcome.reason === 'target_not_archived') {
|
|
128
|
+
const msg = tFn(t, 'cli.memory_restore.target_not_archived', { kind, slug: parsed.slug })
|
|
129
|
+
|| `memory:restore: ${kind} "${parsed.slug}" not found in archive.`;
|
|
130
|
+
log(msg);
|
|
131
|
+
} else if (outcome.reason === 'target_already_active') {
|
|
132
|
+
const msg = tFn(t, 'cli.memory_restore.target_already_active', { kind, slug: parsed.slug })
|
|
133
|
+
|| `memory:restore: ${kind} "${parsed.slug}" already active. No-op.`;
|
|
134
|
+
log(msg);
|
|
135
|
+
} else if (outcome.reason === 'target_not_found') {
|
|
136
|
+
const msg = tFn(t, 'cli.memory_restore.target_not_found', { kind, slug: parsed.slug })
|
|
137
|
+
|| `memory:restore: ${kind} "${parsed.slug}" not found.`;
|
|
138
|
+
log(msg);
|
|
139
|
+
} else {
|
|
140
|
+
log(`memory:restore failed: ${outcome.reason}${outcome.error ? ' — ' + outcome.error : ''}`);
|
|
141
|
+
}
|
|
142
|
+
return outcome;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (wantJson) {
|
|
146
|
+
return {
|
|
147
|
+
ok: true,
|
|
148
|
+
dry_run: Boolean(outcome.dryRun),
|
|
149
|
+
kind,
|
|
150
|
+
slug: parsed.slug,
|
|
151
|
+
source_path: outcome.sourcePath,
|
|
152
|
+
dest_path: outcome.destPath,
|
|
153
|
+
restored_entry_id: outcome.restoredEntryId || null,
|
|
154
|
+
start_at: outcome.startAt || null
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (outcome.dryRun) {
|
|
159
|
+
const msg = tFn(t, 'cli.memory_restore.dry_run_summary', {
|
|
160
|
+
kind,
|
|
161
|
+
slug: parsed.slug,
|
|
162
|
+
source: outcome.sourcePath || kind,
|
|
163
|
+
dest: outcome.destPath || '(no dest)'
|
|
164
|
+
}) || `memory:restore [dry-run]: would move ${outcome.sourcePath || kind} → ${outcome.destPath || '(no dest)'}.`;
|
|
165
|
+
log(msg);
|
|
166
|
+
} else {
|
|
167
|
+
const msg = tFn(t, 'cli.memory_restore.restored_success', {
|
|
168
|
+
kind,
|
|
169
|
+
slug: parsed.slug,
|
|
170
|
+
dest: outcome.destPath || ''
|
|
171
|
+
}) || `memory:restore ✓ ${kind} "${parsed.slug}" restored${outcome.destPath ? ' to ' + outcome.destPath : ''}.`;
|
|
172
|
+
log(msg);
|
|
173
|
+
}
|
|
174
|
+
return outcome;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
module.exports = { runMemoryRestore };
|