@phyzess/pappra 0.0.0-stage → 0.1.0-beta.2
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 +80 -2
- package/THIRD_PARTY_NOTICES.md +16 -0
- package/assets/integrations/skills/pappra/SKILL.md +68 -0
- package/assets/integrations/skills/pappra/references/hosts.md +30 -0
- package/assets/integrations/skills/pappra/references/lifecycle.md +37 -0
- package/assets/integrations/skills/pappra/references/operations.md +14 -0
- package/assets/manifest.json +4 -0
- package/dist/app/collaboration.d.ts +170 -0
- package/dist/app/collaboration.js +1650 -0
- package/dist/app/doctor.d.ts +9 -0
- package/dist/app/doctor.js +1030 -0
- package/dist/app/init.d.ts +143 -0
- package/dist/app/init.js +1111 -0
- package/dist/cli/commands/code.d.ts +16 -0
- package/dist/cli/commands/code.js +127 -0
- package/dist/cli/commands/collaboration.d.ts +13 -0
- package/dist/cli/commands/collaboration.js +186 -0
- package/dist/cli/commands/continuity.d.ts +29 -0
- package/dist/cli/commands/continuity.js +133 -0
- package/dist/cli/commands/docs.d.ts +11 -0
- package/dist/cli/commands/docs.js +133 -0
- package/dist/cli/commands/doctor.d.ts +14 -0
- package/dist/cli/commands/doctor.js +57 -0
- package/dist/cli/commands/host.d.ts +13 -0
- package/dist/cli/commands/host.js +125 -0
- package/dist/cli/commands/init.d.ts +51 -0
- package/dist/cli/commands/init.js +668 -0
- package/dist/cli/main.d.ts +2 -0
- package/dist/cli/main.js +10 -0
- package/dist/cli/output.d.ts +4 -0
- package/dist/cli/output.js +1 -0
- package/dist/cli/run.d.ts +2 -0
- package/dist/cli/run.js +597 -0
- package/dist/code/budget.d.ts +14 -0
- package/dist/code/budget.js +39 -0
- package/dist/code/exec.d.ts +23 -0
- package/dist/code/exec.js +133 -0
- package/dist/code/identity.d.ts +13 -0
- package/dist/code/identity.js +214 -0
- package/dist/code/index.d.ts +6 -0
- package/dist/code/index.js +3 -0
- package/dist/code/providers/codegraph.d.ts +59 -0
- package/dist/code/providers/codegraph.js +567 -0
- package/dist/code/registration.d.ts +9 -0
- package/dist/code/registration.js +293 -0
- package/dist/code/service.d.ts +53 -0
- package/dist/code/service.js +633 -0
- package/dist/code/text.d.ts +3 -0
- package/dist/code/text.js +202 -0
- package/dist/collab/admission.d.ts +450 -0
- package/dist/collab/admission.js +797 -0
- package/dist/collab/canonical.d.ts +26 -0
- package/dist/collab/canonical.js +136 -0
- package/dist/collab/contracts.d.ts +73 -0
- package/dist/collab/contracts.js +76 -0
- package/dist/collab/envelope.d.ts +52 -0
- package/dist/collab/envelope.js +241 -0
- package/dist/collab/fingerprint.d.ts +7 -0
- package/dist/collab/fingerprint.js +58 -0
- package/dist/collab/handoff.d.ts +350 -0
- package/dist/collab/handoff.js +513 -0
- package/dist/collab/index.d.ts +9 -0
- package/dist/collab/index.js +8 -0
- package/dist/collab/lifecycle.d.ts +449 -0
- package/dist/collab/lifecycle.js +534 -0
- package/dist/collab/messages.d.ts +148 -0
- package/dist/collab/messages.js +583 -0
- package/dist/collab/notify.d.ts +47 -0
- package/dist/collab/notify.js +140 -0
- package/dist/collab/preflight.d.ts +31 -0
- package/dist/collab/preflight.js +271 -0
- package/dist/collab/resources.d.ts +119 -0
- package/dist/collab/resources.js +451 -0
- package/dist/collab/store.d.ts +143 -0
- package/dist/collab/store.js +606 -0
- package/dist/collab/types.d.ts +354 -0
- package/dist/collab/types.js +1 -0
- package/dist/collab/validation.d.ts +10 -0
- package/dist/collab/validation.js +303 -0
- package/dist/continuity/context.d.ts +24 -0
- package/dist/continuity/context.js +179 -0
- package/dist/continuity/index.d.ts +3 -0
- package/dist/continuity/index.js +1 -0
- package/dist/continuity/paths.d.ts +37 -0
- package/dist/continuity/paths.js +258 -0
- package/dist/continuity/service.d.ts +59 -0
- package/dist/continuity/service.js +1258 -0
- package/dist/hosts/claude/native.d.ts +79 -0
- package/dist/hosts/claude/native.js +853 -0
- package/dist/hosts/codex/native.d.ts +345 -0
- package/dist/hosts/codex/native.js +1528 -0
- package/dist/hosts/codex/protocol.d.ts +51 -0
- package/dist/hosts/codex/protocol.js +42 -0
- package/dist/hosts/detect.d.ts +28 -0
- package/dist/hosts/detect.js +73 -0
- package/dist/hosts/index.d.ts +10 -0
- package/dist/hosts/index.js +10 -0
- package/dist/hosts/integration/bundle.d.ts +16 -0
- package/dist/hosts/integration/bundle.js +81 -0
- package/dist/hosts/integration/hook-entry.d.ts +1 -0
- package/dist/hosts/integration/hook-entry.js +68 -0
- package/dist/hosts/integration/hook.d.ts +32 -0
- package/dist/hosts/integration/hook.js +781 -0
- package/dist/hosts/integration/pi-extension-runtime.d.ts +18 -0
- package/dist/hosts/integration/pi-extension-runtime.js +89 -0
- package/dist/hosts/integration/pi-template.d.ts +3 -0
- package/dist/hosts/integration/pi-template.js +5 -0
- package/dist/hosts/integration/service.d.ts +41 -0
- package/dist/hosts/integration/service.js +1381 -0
- package/dist/hosts/integration/support.d.ts +36 -0
- package/dist/hosts/integration/support.js +41 -0
- package/dist/hosts/integration/templates.d.ts +22 -0
- package/dist/hosts/integration/templates.js +70 -0
- package/dist/hosts/pi/protocol.d.ts +22 -0
- package/dist/hosts/pi/protocol.js +28 -0
- package/dist/hosts/pi/query-result.d.ts +22 -0
- package/dist/hosts/pi/query-result.js +710 -0
- package/dist/hosts/pi/rpc.d.ts +937 -0
- package/dist/hosts/pi/rpc.js +1621 -0
- package/dist/hosts/pilot.d.ts +485 -0
- package/dist/hosts/pilot.js +593 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +4 -0
- package/dist/infra/fs.d.ts +27 -0
- package/dist/infra/fs.js +130 -0
- package/dist/infra/git.d.ts +60 -0
- package/dist/infra/git.js +358 -0
- package/dist/infra/owned-process-group.d.ts +173 -0
- package/dist/infra/owned-process-group.js +571 -0
- package/dist/install/apply.d.ts +65 -0
- package/dist/install/apply.js +663 -0
- package/dist/install/index.d.ts +5 -0
- package/dist/install/index.js +5 -0
- package/dist/install/ownership.d.ts +85 -0
- package/dist/install/ownership.js +430 -0
- package/dist/install/plan.d.ts +39 -0
- package/dist/install/plan.js +97 -0
- package/dist/install/rollback.d.ts +18 -0
- package/dist/install/rollback.js +544 -0
- package/dist/install/runtime.d.ts +76 -0
- package/dist/install/runtime.js +1145 -0
- package/dist/knowledge/catalog.d.ts +47 -0
- package/dist/knowledge/catalog.js +308 -0
- package/dist/knowledge/context.d.ts +28 -0
- package/dist/knowledge/context.js +61 -0
- package/dist/knowledge/index.d.ts +3 -0
- package/dist/knowledge/index.js +1 -0
- package/dist/knowledge/journal.d.ts +16 -0
- package/dist/knowledge/journal.js +109 -0
- package/dist/knowledge/lifecycle.d.ts +8 -0
- package/dist/knowledge/lifecycle.js +1001 -0
- package/dist/knowledge/paths.d.ts +37 -0
- package/dist/knowledge/paths.js +236 -0
- package/dist/knowledge/refs.d.ts +36 -0
- package/dist/knowledge/refs.js +267 -0
- package/dist/knowledge/service.d.ts +21 -0
- package/dist/knowledge/service.js +393 -0
- package/dist/knowledge/types.d.ts +171 -0
- package/dist/knowledge/types.js +1 -0
- package/dist/knowledge/write.d.ts +8 -0
- package/dist/knowledge/write.js +1223 -0
- package/dist/package-info.d.ts +6 -0
- package/dist/package-info.js +19 -0
- package/dist/schemas/code.d.ts +377 -0
- package/dist/schemas/code.js +145 -0
- package/dist/schemas/collaboration.d.ts +115 -0
- package/dist/schemas/collaboration.js +106 -0
- package/dist/schemas/continuity.d.ts +315 -0
- package/dist/schemas/continuity.js +199 -0
- package/dist/schemas/diagnostics.d.ts +28 -0
- package/dist/schemas/diagnostics.js +27 -0
- package/dist/schemas/doctor.d.ts +157 -0
- package/dist/schemas/doctor.js +75 -0
- package/dist/schemas/document.d.ts +644 -0
- package/dist/schemas/document.js +296 -0
- package/dist/schemas/errors.d.ts +177 -0
- package/dist/schemas/errors.js +176 -0
- package/dist/schemas/evidence.d.ts +24 -0
- package/dist/schemas/evidence.js +16 -0
- package/dist/schemas/host.d.ts +436 -0
- package/dist/schemas/host.js +201 -0
- package/dist/schemas/index.d.ts +15 -0
- package/dist/schemas/index.js +15 -0
- package/dist/schemas/init.d.ts +121 -0
- package/dist/schemas/init.js +94 -0
- package/dist/schemas/install.d.ts +213 -0
- package/dist/schemas/install.js +139 -0
- package/dist/schemas/machine.d.ts +43 -0
- package/dist/schemas/machine.js +96 -0
- package/dist/schemas/outcome.d.ts +23 -0
- package/dist/schemas/outcome.js +17 -0
- package/dist/schemas/paths.d.ts +19 -0
- package/dist/schemas/paths.js +63 -0
- package/dist/schemas/workspace.d.ts +120 -0
- package/dist/schemas/workspace.js +201 -0
- package/dist/workspace/config-file.d.ts +36 -0
- package/dist/workspace/config-file.js +237 -0
- package/dist/workspace/config.d.ts +28 -0
- package/dist/workspace/config.js +79 -0
- package/dist/workspace/discover.d.ts +34 -0
- package/dist/workspace/discover.js +195 -0
- package/dist/workspace/documents.d.ts +21 -0
- package/dist/workspace/documents.js +395 -0
- package/dist/workspace/ignore.d.ts +10 -0
- package/dist/workspace/ignore.js +48 -0
- package/dist/workspace/index.d.ts +7 -0
- package/dist/workspace/index.js +3 -0
- package/dist/workspace/repositories.d.ts +24 -0
- package/dist/workspace/repositories.js +411 -0
- package/dist/workspace/resolve.d.ts +24 -0
- package/dist/workspace/resolve.js +268 -0
- package/dist/workspace/types.d.ts +71 -0
- package/dist/workspace/types.js +1 -0
- package/dist/workspace/validation.d.ts +18 -0
- package/dist/workspace/validation.js +197 -0
- package/package.json +105 -4
- package/skills/pappra/SKILL.md +68 -0
- package/skills/pappra/references/hosts.md +30 -0
- package/skills/pappra/references/lifecycle.md +37 -0
- package/skills/pappra/references/operations.md +14 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { z } from 'zod';
|
|
2
|
+
import type { Diagnostic } from '../schemas/diagnostics.js';
|
|
3
|
+
import type { Outcome } from '../schemas/outcome.js';
|
|
4
|
+
import { fileIdentitySchema, type InstallJournal, type InstallLock, type ManagedFileEntry, type WorkspaceInstallManifest } from '../schemas/install.js';
|
|
5
|
+
export interface WorkspaceInstallPaths {
|
|
6
|
+
readonly root: string;
|
|
7
|
+
readonly localDir: string;
|
|
8
|
+
readonly manifestPath: string;
|
|
9
|
+
readonly lockPath: string;
|
|
10
|
+
readonly journalsDir: string;
|
|
11
|
+
readonly backupsDir: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function workspaceInstallPaths(root: string): WorkspaceInstallPaths;
|
|
14
|
+
export declare function journalPath(root: string, installId: string): string;
|
|
15
|
+
export declare function backupPath(root: string, installId: string, relativePath: string): string;
|
|
16
|
+
/**
|
|
17
|
+
* Checks that a managed state path and every existing ancestor stay inside the
|
|
18
|
+
* allowed root. Metadata writes must not follow a symlinked parent outside it.
|
|
19
|
+
*/
|
|
20
|
+
export declare function ensureStatePathBoundary(root: string, path: string): Promise<Outcome<null>>;
|
|
21
|
+
export declare function ensureWorkspaceStateBoundary(root: string): Promise<Outcome<null>>;
|
|
22
|
+
export interface JsonReadResult<T> {
|
|
23
|
+
readonly exists: boolean;
|
|
24
|
+
readonly value: T | null;
|
|
25
|
+
readonly digest: string | null;
|
|
26
|
+
readonly diagnostics: readonly Diagnostic[];
|
|
27
|
+
}
|
|
28
|
+
export declare function readJsonDocument<T>(path: string, schema: z.ZodType<T>): Promise<JsonReadResult<T>>;
|
|
29
|
+
export declare function writeBytesAtomic(path: string, bytes: Buffer, boundaryRoot?: string): Promise<Outcome<{
|
|
30
|
+
readonly digest: string;
|
|
31
|
+
}>>;
|
|
32
|
+
export declare function writeJsonAtomic(path: string, value: unknown, boundaryRoot?: string): Promise<Outcome<{
|
|
33
|
+
readonly digest: string;
|
|
34
|
+
}>>;
|
|
35
|
+
export interface LockAcquireResult {
|
|
36
|
+
readonly ok: boolean;
|
|
37
|
+
readonly status: 'acquired' | 'locked' | 'failed';
|
|
38
|
+
readonly lock: InstallLock | null;
|
|
39
|
+
readonly path: string;
|
|
40
|
+
readonly message: string | null;
|
|
41
|
+
readonly existing: InstallLock | null;
|
|
42
|
+
}
|
|
43
|
+
export declare function acquireInstallLock(path: string, operation: InstallLock['operation'], target: string, boundaryRoot?: string): Promise<LockAcquireResult>;
|
|
44
|
+
export declare function releaseInstallLock(path: string, token: string): Promise<Outcome<null>>;
|
|
45
|
+
export declare function readWorkspaceManifest(root: string): Promise<JsonReadResult<WorkspaceInstallManifest>>;
|
|
46
|
+
export declare function readInstallJournal(path: string): Promise<JsonReadResult<InstallJournal>>;
|
|
47
|
+
export interface JournalDirectoryScan {
|
|
48
|
+
readonly kind: 'missing' | 'directory' | 'unreadable';
|
|
49
|
+
readonly files: readonly string[];
|
|
50
|
+
readonly nonRegular: readonly string[];
|
|
51
|
+
readonly error: string | null;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Enumerates `.json` journal candidates without following symlinks or opening
|
|
55
|
+
* them. Only regular files are returned as candidates; every other `.json`
|
|
56
|
+
* entry is reported as non-regular so callers can fail closed instead of
|
|
57
|
+
* treating a broken link, directory, FIFO, or device as absent.
|
|
58
|
+
*/
|
|
59
|
+
export declare function scanJournalDirectory(directory: string): Promise<JournalDirectoryScan>;
|
|
60
|
+
export interface FileOwnershipState {
|
|
61
|
+
readonly status: 'match' | 'missing' | 'modified' | 'unsafe';
|
|
62
|
+
readonly identity: {
|
|
63
|
+
readonly dev: number;
|
|
64
|
+
readonly ino: number;
|
|
65
|
+
} | null;
|
|
66
|
+
readonly digest: string | null;
|
|
67
|
+
}
|
|
68
|
+
export declare function inspectOwnedFile(root: string, entry: ManagedFileEntry): Promise<FileOwnershipState>;
|
|
69
|
+
export declare function removeOwnedFile(root: string, entry: ManagedFileEntry): Promise<Outcome<null>>;
|
|
70
|
+
export declare function createManifest(root: string, rootIdentity: {
|
|
71
|
+
readonly dev: number;
|
|
72
|
+
readonly ino: number;
|
|
73
|
+
}): WorkspaceInstallManifest;
|
|
74
|
+
export declare function writeWorkspaceManifest(root: string, manifest: WorkspaceInstallManifest): Promise<Outcome<{
|
|
75
|
+
readonly digest: string;
|
|
76
|
+
}>>;
|
|
77
|
+
export declare function writeInstallJournal(path: string, journal: InstallJournal, boundaryRoot?: string): Promise<Outcome<{
|
|
78
|
+
readonly digest: string;
|
|
79
|
+
}>>;
|
|
80
|
+
export declare function rootIdentityOf(root: string): Promise<{
|
|
81
|
+
readonly dev: number;
|
|
82
|
+
readonly ino: number;
|
|
83
|
+
} | null>;
|
|
84
|
+
export declare function resolveManagedRoot(root: string): Promise<string | null>;
|
|
85
|
+
export { fileIdentitySchema };
|
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { mkdir, open, readFile, readdir, rename, rm, unlink, } from 'node:fs/promises';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { createDiagnostic } from '../schemas/diagnostics.js';
|
|
5
|
+
import { diagnosticCodes } from '../schemas/errors.js';
|
|
6
|
+
import { errorMessage, failed, succeeded } from '../schemas/outcome.js';
|
|
7
|
+
import { fileIdentitySchema, installJournalSchema, installLockSchema, workspaceInstallManifestSchema, } from '../schemas/install.js';
|
|
8
|
+
import { isPathInside, samePath } from '../schemas/paths.js';
|
|
9
|
+
import { observePath, readTextFileBounded, resolveRealpath, sha256Digest, } from '../infra/fs.js';
|
|
10
|
+
export function workspaceInstallPaths(root) {
|
|
11
|
+
const localDir = join(root, '.pappra', 'local');
|
|
12
|
+
return {
|
|
13
|
+
root,
|
|
14
|
+
localDir,
|
|
15
|
+
manifestPath: join(localDir, 'install-manifest.json'),
|
|
16
|
+
lockPath: join(localDir, 'apply.lock'),
|
|
17
|
+
journalsDir: join(localDir, 'journals'),
|
|
18
|
+
backupsDir: join(localDir, 'backups'),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export function journalPath(root, installId) {
|
|
22
|
+
return join(workspaceInstallPaths(root).journalsDir, `${installId}.json`);
|
|
23
|
+
}
|
|
24
|
+
export function backupPath(root, installId, relativePath) {
|
|
25
|
+
return join(workspaceInstallPaths(root).backupsDir, installId, ...relativePath.split('/'));
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Checks that a managed state path and every existing ancestor stay inside the
|
|
29
|
+
* allowed root. Metadata writes must not follow a symlinked parent outside it.
|
|
30
|
+
*/
|
|
31
|
+
export async function ensureStatePathBoundary(root, path) {
|
|
32
|
+
if (!isPathInside(root, path)) {
|
|
33
|
+
return failed(diagnosticCodes.initTargetUnsafe, `Managed state path escapes the root: ${path}`, {
|
|
34
|
+
nextAction: 'Inspect or remove the symlinked state directory explicitly.',
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
const realRoot = await resolveRealpath(root);
|
|
38
|
+
if (realRoot === null) {
|
|
39
|
+
const rootObservation = await observePath(root);
|
|
40
|
+
if (rootObservation.kind === 'missing') {
|
|
41
|
+
// A user-data directory may not exist before the first install. Its
|
|
42
|
+
// lexical descendants are still checked against the eventual root.
|
|
43
|
+
return succeeded(null);
|
|
44
|
+
}
|
|
45
|
+
return failed(diagnosticCodes.initTargetUnsafe, `Cannot resolve managed state root: ${root}`, { nextAction: 'Retry from an existing workspace root.' });
|
|
46
|
+
}
|
|
47
|
+
let ancestor = dirname(path);
|
|
48
|
+
for (;;) {
|
|
49
|
+
const observation = await observePath(ancestor);
|
|
50
|
+
if (observation.kind !== 'missing') {
|
|
51
|
+
if (observation.kind !== 'directory') {
|
|
52
|
+
return failed(diagnosticCodes.initTargetUnsafe, `Managed state parent is not a directory: ${ancestor}`, {
|
|
53
|
+
nextAction: 'Inspect the state path; Pappra does not write through it.',
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
const real = await resolveRealpath(ancestor);
|
|
57
|
+
if (real === null || !isPathInside(realRoot, real)) {
|
|
58
|
+
return failed(diagnosticCodes.initTargetUnsafe, `Managed state parent escapes the workspace: ${ancestor}`, {
|
|
59
|
+
nextAction: 'Inspect or remove the symlink explicitly; no metadata was written.',
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
return succeeded(null);
|
|
63
|
+
}
|
|
64
|
+
if (samePath(ancestor, root))
|
|
65
|
+
return succeeded(null);
|
|
66
|
+
const parent = dirname(ancestor);
|
|
67
|
+
if (samePath(parent, ancestor)) {
|
|
68
|
+
return failed(diagnosticCodes.initTargetUnsafe, `Cannot establish a managed state boundary for: ${path}`, { nextAction: 'Inspect the workspace path.' });
|
|
69
|
+
}
|
|
70
|
+
ancestor = parent;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
export async function ensureWorkspaceStateBoundary(root) {
|
|
74
|
+
return ensureStatePathBoundary(root, workspaceInstallPaths(root).localDir);
|
|
75
|
+
}
|
|
76
|
+
export async function readJsonDocument(path, schema) {
|
|
77
|
+
const read = await readTextFileBounded(path);
|
|
78
|
+
if (!read.ok) {
|
|
79
|
+
if (read.code === diagnosticCodes.fileNotFound) {
|
|
80
|
+
return { exists: false, value: null, digest: null, diagnostics: [] };
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
exists: true,
|
|
84
|
+
value: null,
|
|
85
|
+
digest: null,
|
|
86
|
+
diagnostics: [
|
|
87
|
+
createDiagnostic({
|
|
88
|
+
code: read.code,
|
|
89
|
+
severity: 'error',
|
|
90
|
+
message: read.message,
|
|
91
|
+
source: path,
|
|
92
|
+
hint: read.nextAction,
|
|
93
|
+
}),
|
|
94
|
+
],
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
let raw;
|
|
98
|
+
try {
|
|
99
|
+
raw = JSON.parse(read.value.text);
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
return {
|
|
103
|
+
exists: true,
|
|
104
|
+
value: null,
|
|
105
|
+
digest: read.value.digest,
|
|
106
|
+
diagnostics: [
|
|
107
|
+
createDiagnostic({
|
|
108
|
+
code: diagnosticCodes.configInvalidJson,
|
|
109
|
+
severity: 'error',
|
|
110
|
+
message: `Invalid JSON: ${errorMessage(error)}`,
|
|
111
|
+
source: path,
|
|
112
|
+
hint: 'Inspect and repair this file; Pappra will not overwrite it automatically.',
|
|
113
|
+
}),
|
|
114
|
+
],
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
const parsed = schema.safeParse(raw);
|
|
118
|
+
if (!parsed.success) {
|
|
119
|
+
return {
|
|
120
|
+
exists: true,
|
|
121
|
+
value: null,
|
|
122
|
+
digest: read.value.digest,
|
|
123
|
+
diagnostics: [
|
|
124
|
+
createDiagnostic({
|
|
125
|
+
code: diagnosticCodes.configSchemaInvalid,
|
|
126
|
+
severity: 'error',
|
|
127
|
+
message: parsed.error.issues[0]?.message ?? 'Invalid installation state.',
|
|
128
|
+
source: path,
|
|
129
|
+
hint: 'Inspect the state file; Pappra fails closed instead of replacing unknown data.',
|
|
130
|
+
}),
|
|
131
|
+
],
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
exists: true,
|
|
136
|
+
value: parsed.data,
|
|
137
|
+
digest: read.value.digest,
|
|
138
|
+
diagnostics: [],
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
export async function writeBytesAtomic(path, bytes, boundaryRoot) {
|
|
142
|
+
if (boundaryRoot !== undefined) {
|
|
143
|
+
const boundary = await ensureStatePathBoundary(boundaryRoot, path);
|
|
144
|
+
if (!boundary.ok)
|
|
145
|
+
return boundary;
|
|
146
|
+
}
|
|
147
|
+
const temporary = `${path}.tmp-${randomUUID()}`;
|
|
148
|
+
try {
|
|
149
|
+
await mkdir(dirname(path), { recursive: true });
|
|
150
|
+
await writeNewFile(temporary, bytes);
|
|
151
|
+
await rename(temporary, path);
|
|
152
|
+
return succeeded({ digest: sha256Digest(bytes) });
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
await rm(temporary, { force: true }).catch(() => undefined);
|
|
156
|
+
return failed(diagnosticCodes.fileUnreadable, `Cannot write ${path}: ${errorMessage(error)}`, { nextAction: 'Inspect permissions and available disk space.' });
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
export async function writeJsonAtomic(path, value, boundaryRoot) {
|
|
160
|
+
const bytes = Buffer.from(`${JSON.stringify(value, null, 2)}\n`, 'utf8');
|
|
161
|
+
return writeBytesAtomic(path, bytes, boundaryRoot);
|
|
162
|
+
}
|
|
163
|
+
async function writeNewFile(path, bytes) {
|
|
164
|
+
const handle = await open(path, 'wx');
|
|
165
|
+
try {
|
|
166
|
+
await handle.writeFile(bytes);
|
|
167
|
+
}
|
|
168
|
+
finally {
|
|
169
|
+
await handle.close().catch(() => undefined);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
export async function acquireInstallLock(path, operation, target, boundaryRoot) {
|
|
173
|
+
if (boundaryRoot !== undefined) {
|
|
174
|
+
const boundary = await ensureStatePathBoundary(boundaryRoot, path);
|
|
175
|
+
if (!boundary.ok) {
|
|
176
|
+
return {
|
|
177
|
+
ok: false,
|
|
178
|
+
status: 'failed',
|
|
179
|
+
lock: null,
|
|
180
|
+
path,
|
|
181
|
+
message: boundary.message,
|
|
182
|
+
existing: null,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
try {
|
|
187
|
+
await mkdir(dirname(path), { recursive: true });
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
return {
|
|
191
|
+
ok: false,
|
|
192
|
+
status: 'failed',
|
|
193
|
+
lock: null,
|
|
194
|
+
path,
|
|
195
|
+
message: `Cannot create installation lock directory: ${errorMessage(error)}`,
|
|
196
|
+
existing: null,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
const lock = {
|
|
200
|
+
schema: 'pappra/install-lock/1',
|
|
201
|
+
token: randomUUID(),
|
|
202
|
+
operation,
|
|
203
|
+
target,
|
|
204
|
+
pid: process.pid,
|
|
205
|
+
createdAt: new Date().toISOString(),
|
|
206
|
+
};
|
|
207
|
+
try {
|
|
208
|
+
await writeNewFile(path, Buffer.from(`${JSON.stringify(lock, null, 2)}\n`));
|
|
209
|
+
return {
|
|
210
|
+
ok: true,
|
|
211
|
+
status: 'acquired',
|
|
212
|
+
lock,
|
|
213
|
+
path,
|
|
214
|
+
message: null,
|
|
215
|
+
existing: null,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
const existingRead = await readJsonDocument(path, installLockSchema);
|
|
220
|
+
const code = typeof error === 'object' &&
|
|
221
|
+
error !== null &&
|
|
222
|
+
'code' in error &&
|
|
223
|
+
error.code === 'EEXIST'
|
|
224
|
+
? 'locked'
|
|
225
|
+
: 'failed';
|
|
226
|
+
return {
|
|
227
|
+
ok: false,
|
|
228
|
+
status: code,
|
|
229
|
+
lock: null,
|
|
230
|
+
path,
|
|
231
|
+
message: code === 'locked'
|
|
232
|
+
? `Installation lock is already held: ${path}`
|
|
233
|
+
: `Cannot acquire installation lock ${path}: ${errorMessage(error)}`,
|
|
234
|
+
existing: existingRead.value,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
export async function releaseInstallLock(path, token) {
|
|
239
|
+
const read = await readJsonDocument(path, installLockSchema);
|
|
240
|
+
if (!read.exists)
|
|
241
|
+
return succeeded(null);
|
|
242
|
+
if (read.value === null) {
|
|
243
|
+
return failed(diagnosticCodes.initApplyResidual, `Cannot verify install lock ownership: ${path}`, { nextAction: 'Inspect the lock manually; it was not removed.' });
|
|
244
|
+
}
|
|
245
|
+
if (read.value.token !== token) {
|
|
246
|
+
return failed(diagnosticCodes.initApplyResidual, `Install lock belongs to another owner: ${path}`, { nextAction: 'Inspect the lock manually; it was not removed.' });
|
|
247
|
+
}
|
|
248
|
+
try {
|
|
249
|
+
await unlink(path);
|
|
250
|
+
return succeeded(null);
|
|
251
|
+
}
|
|
252
|
+
catch (error) {
|
|
253
|
+
return failed(diagnosticCodes.initApplyResidual, `Cannot remove install lock ${path}: ${errorMessage(error)}`, { nextAction: 'Inspect the lock manually; it remains as evidence.' });
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
export async function readWorkspaceManifest(root) {
|
|
257
|
+
return readJsonDocument(workspaceInstallPaths(root).manifestPath, workspaceInstallManifestSchema);
|
|
258
|
+
}
|
|
259
|
+
export async function readInstallJournal(path) {
|
|
260
|
+
return readJsonDocument(path, installJournalSchema);
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Enumerates `.json` journal candidates without following symlinks or opening
|
|
264
|
+
* them. Only regular files are returned as candidates; every other `.json`
|
|
265
|
+
* entry is reported as non-regular so callers can fail closed instead of
|
|
266
|
+
* treating a broken link, directory, FIFO, or device as absent.
|
|
267
|
+
*/
|
|
268
|
+
export async function scanJournalDirectory(directory) {
|
|
269
|
+
let observation;
|
|
270
|
+
try {
|
|
271
|
+
observation = await observePath(directory);
|
|
272
|
+
}
|
|
273
|
+
catch (error) {
|
|
274
|
+
return {
|
|
275
|
+
kind: 'unreadable',
|
|
276
|
+
files: [],
|
|
277
|
+
nonRegular: [],
|
|
278
|
+
error: errorMessage(error),
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
if (observation.kind === 'missing') {
|
|
282
|
+
return { kind: 'missing', files: [], nonRegular: [], error: null };
|
|
283
|
+
}
|
|
284
|
+
if (observation.kind !== 'directory') {
|
|
285
|
+
return {
|
|
286
|
+
kind: 'unreadable',
|
|
287
|
+
files: [],
|
|
288
|
+
nonRegular: [],
|
|
289
|
+
error: `Journal path is not a directory: ${directory}`,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
let entries;
|
|
293
|
+
try {
|
|
294
|
+
entries = await readdir(directory, { withFileTypes: true });
|
|
295
|
+
}
|
|
296
|
+
catch (error) {
|
|
297
|
+
const code = typeof error === 'object' &&
|
|
298
|
+
error !== null &&
|
|
299
|
+
'code' in error &&
|
|
300
|
+
typeof error.code === 'string'
|
|
301
|
+
? error.code
|
|
302
|
+
: null;
|
|
303
|
+
if (code === 'ENOENT') {
|
|
304
|
+
return { kind: 'missing', files: [], nonRegular: [], error: null };
|
|
305
|
+
}
|
|
306
|
+
return {
|
|
307
|
+
kind: 'unreadable',
|
|
308
|
+
files: [],
|
|
309
|
+
nonRegular: [],
|
|
310
|
+
error: errorMessage(error),
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
entries.sort((left, right) => left.name.localeCompare(right.name));
|
|
314
|
+
const files = [];
|
|
315
|
+
const nonRegular = [];
|
|
316
|
+
for (const entry of entries) {
|
|
317
|
+
if (!entry.name.endsWith('.json'))
|
|
318
|
+
continue;
|
|
319
|
+
const path = join(directory, entry.name);
|
|
320
|
+
if (entry.isFile()) {
|
|
321
|
+
files.push(path);
|
|
322
|
+
}
|
|
323
|
+
else {
|
|
324
|
+
nonRegular.push(path);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return { kind: 'directory', files, nonRegular, error: null };
|
|
328
|
+
}
|
|
329
|
+
export async function inspectOwnedFile(root, entry) {
|
|
330
|
+
const absolute = join(root, ...entry.path.split('/'));
|
|
331
|
+
if (!isPathInside(root, absolute)) {
|
|
332
|
+
return { status: 'unsafe', identity: null, digest: null };
|
|
333
|
+
}
|
|
334
|
+
const observation = await observePath(absolute).catch(() => null);
|
|
335
|
+
if (observation === null) {
|
|
336
|
+
return { status: 'unsafe', identity: null, digest: null };
|
|
337
|
+
}
|
|
338
|
+
if (observation.kind === 'missing') {
|
|
339
|
+
return { status: 'missing', identity: null, digest: null };
|
|
340
|
+
}
|
|
341
|
+
const realRoot = await resolveRealpath(root);
|
|
342
|
+
const realPath = await resolveRealpath(absolute);
|
|
343
|
+
if (realRoot === null ||
|
|
344
|
+
realPath === null ||
|
|
345
|
+
!isPathInside(realRoot, realPath)) {
|
|
346
|
+
return { status: 'unsafe', identity: null, digest: null };
|
|
347
|
+
}
|
|
348
|
+
if (observation.kind !== 'file' || observation.stats === null) {
|
|
349
|
+
return { status: 'modified', identity: null, digest: null };
|
|
350
|
+
}
|
|
351
|
+
const identity = {
|
|
352
|
+
dev: observation.stats.dev,
|
|
353
|
+
ino: observation.stats.ino,
|
|
354
|
+
};
|
|
355
|
+
let bytes;
|
|
356
|
+
try {
|
|
357
|
+
bytes = await readFile(absolute);
|
|
358
|
+
}
|
|
359
|
+
catch {
|
|
360
|
+
return { status: 'unsafe', identity, digest: null };
|
|
361
|
+
}
|
|
362
|
+
const digest = sha256Digest(bytes);
|
|
363
|
+
const matches = identity.dev === entry.identity.dev &&
|
|
364
|
+
identity.ino === entry.identity.ino &&
|
|
365
|
+
digest === entry.afterDigest;
|
|
366
|
+
return {
|
|
367
|
+
status: matches ? 'match' : 'modified',
|
|
368
|
+
identity,
|
|
369
|
+
digest,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
export async function removeOwnedFile(root, entry) {
|
|
373
|
+
const state = await inspectOwnedFile(root, entry);
|
|
374
|
+
if (state.status === 'missing')
|
|
375
|
+
return succeeded(null);
|
|
376
|
+
if (state.status !== 'match') {
|
|
377
|
+
return failed(diagnosticCodes.initApplyResidual, `Managed file no longer matches its recorded identity/content: ${entry.path}`, {
|
|
378
|
+
nextAction: 'Keep the user-modified file; remove ownership only after review.',
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
try {
|
|
382
|
+
await unlink(join(root, ...entry.path.split('/')));
|
|
383
|
+
return succeeded(null);
|
|
384
|
+
}
|
|
385
|
+
catch (error) {
|
|
386
|
+
return failed(diagnosticCodes.initApplyResidual, `Cannot remove managed file ${entry.path}: ${errorMessage(error)}`, { nextAction: 'Inspect the file and permissions; it was not removed.' });
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
export function createManifest(root, rootIdentity) {
|
|
390
|
+
const now = new Date().toISOString();
|
|
391
|
+
return {
|
|
392
|
+
schema: 'pappra/install-manifest/1',
|
|
393
|
+
workspaceRoot: root,
|
|
394
|
+
rootIdentity,
|
|
395
|
+
entries: [],
|
|
396
|
+
createdAt: now,
|
|
397
|
+
updatedAt: now,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
export async function writeWorkspaceManifest(root, manifest) {
|
|
401
|
+
const parsed = workspaceInstallManifestSchema.safeParse({
|
|
402
|
+
...manifest,
|
|
403
|
+
updatedAt: new Date().toISOString(),
|
|
404
|
+
});
|
|
405
|
+
if (!parsed.success) {
|
|
406
|
+
return failed(diagnosticCodes.initPlanInvalid, `Managed manifest is invalid: ${parsed.error.issues[0]?.message ?? 'unknown'}`, { nextAction: 'Inspect the workspace ownership state.' });
|
|
407
|
+
}
|
|
408
|
+
return writeJsonAtomic(workspaceInstallPaths(root).manifestPath, parsed.data, root);
|
|
409
|
+
}
|
|
410
|
+
export async function writeInstallJournal(path, journal, boundaryRoot) {
|
|
411
|
+
const parsed = installJournalSchema.safeParse({
|
|
412
|
+
...journal,
|
|
413
|
+
updatedAt: new Date().toISOString(),
|
|
414
|
+
});
|
|
415
|
+
if (!parsed.success) {
|
|
416
|
+
return failed(diagnosticCodes.initPlanInvalid, `Install journal is invalid: ${parsed.error.issues[0]?.message ?? 'unknown'}`, { nextAction: 'Inspect the installation state.' });
|
|
417
|
+
}
|
|
418
|
+
return writeJsonAtomic(path, parsed.data, boundaryRoot);
|
|
419
|
+
}
|
|
420
|
+
export async function rootIdentityOf(root) {
|
|
421
|
+
const observation = await observePath(root);
|
|
422
|
+
return observation.kind === 'directory' && observation.stats !== null
|
|
423
|
+
? { dev: observation.stats.dev, ino: observation.stats.ino }
|
|
424
|
+
: null;
|
|
425
|
+
}
|
|
426
|
+
export async function resolveManagedRoot(root) {
|
|
427
|
+
const real = await resolveRealpath(root);
|
|
428
|
+
return real;
|
|
429
|
+
}
|
|
430
|
+
export { fileIdentitySchema };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { InitExternalAction, InitPlan } from '../schemas/init.js';
|
|
2
|
+
import { type PlannedFileOperation } from '../schemas/init.js';
|
|
3
|
+
export interface PlannedFileContent {
|
|
4
|
+
readonly path: string;
|
|
5
|
+
readonly operation: PlannedFileOperation;
|
|
6
|
+
readonly bytes: Buffer;
|
|
7
|
+
readonly digest: string;
|
|
8
|
+
readonly diff: string;
|
|
9
|
+
}
|
|
10
|
+
export interface InitFileInput {
|
|
11
|
+
readonly path: string;
|
|
12
|
+
readonly bytes: Buffer;
|
|
13
|
+
readonly beforeText?: string | null;
|
|
14
|
+
readonly operation?: PlannedFileOperation;
|
|
15
|
+
readonly expectedBeforeDigest?: string | null;
|
|
16
|
+
readonly backupRequired?: boolean;
|
|
17
|
+
}
|
|
18
|
+
export interface BuildInitPlanInput {
|
|
19
|
+
readonly workspaceRoot: string;
|
|
20
|
+
readonly configurationDigest: string;
|
|
21
|
+
readonly files: readonly InitFileInput[];
|
|
22
|
+
readonly externalActions?: readonly InitExternalAction[];
|
|
23
|
+
readonly planId?: string;
|
|
24
|
+
readonly discoveredAt?: string;
|
|
25
|
+
}
|
|
26
|
+
export interface BuiltInitPlan {
|
|
27
|
+
readonly plan: InitPlan;
|
|
28
|
+
readonly writes: readonly PlannedFileContent[];
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Produces a bounded, human-reviewable line diff. P03 plans new files; the
|
|
32
|
+
* replacement path is retained for future managed-file operations.
|
|
33
|
+
*/
|
|
34
|
+
export declare function unifiedFileDiff(path: string, before: string | null, after: string): string;
|
|
35
|
+
/**
|
|
36
|
+
* Builds the strict persisted InitPlan plus the exact bytes needed by apply.
|
|
37
|
+
* Plan JSON never includes file contents; diffs stay in the runtime bundle.
|
|
38
|
+
*/
|
|
39
|
+
export declare function buildInitPlan(input: BuildInitPlanInput): BuiltInitPlan;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { initPlanSchema } from '../schemas/init.js';
|
|
3
|
+
import { isPortableRelativePath } from '../schemas/paths.js';
|
|
4
|
+
import { sha256Digest } from '../infra/fs.js';
|
|
5
|
+
function splitLines(value) {
|
|
6
|
+
if (value === '')
|
|
7
|
+
return [];
|
|
8
|
+
const lines = value.split('\n');
|
|
9
|
+
if (lines.at(-1) === '')
|
|
10
|
+
lines.pop();
|
|
11
|
+
return lines;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Produces a bounded, human-reviewable line diff. P03 plans new files; the
|
|
15
|
+
* replacement path is retained for future managed-file operations.
|
|
16
|
+
*/
|
|
17
|
+
export function unifiedFileDiff(path, before, after) {
|
|
18
|
+
if (before === after)
|
|
19
|
+
return '';
|
|
20
|
+
const beforeLines = splitLines(before ?? '');
|
|
21
|
+
const afterLines = splitLines(after);
|
|
22
|
+
const lines = [
|
|
23
|
+
before === null ? '--- /dev/null' : `--- a/${path}`,
|
|
24
|
+
`+++ b/${path}`,
|
|
25
|
+
];
|
|
26
|
+
if (before === null) {
|
|
27
|
+
lines.push(`@@ -0,0 +1,${afterLines.length} @@`);
|
|
28
|
+
for (const line of afterLines)
|
|
29
|
+
lines.push(`+${line}`);
|
|
30
|
+
return `${lines.join('\n')}\n`;
|
|
31
|
+
}
|
|
32
|
+
let prefix = 0;
|
|
33
|
+
while (prefix < beforeLines.length &&
|
|
34
|
+
prefix < afterLines.length &&
|
|
35
|
+
beforeLines[prefix] === afterLines[prefix]) {
|
|
36
|
+
prefix += 1;
|
|
37
|
+
}
|
|
38
|
+
let suffix = 0;
|
|
39
|
+
while (suffix < beforeLines.length - prefix &&
|
|
40
|
+
suffix < afterLines.length - prefix &&
|
|
41
|
+
beforeLines[beforeLines.length - 1 - suffix] ===
|
|
42
|
+
afterLines[afterLines.length - 1 - suffix]) {
|
|
43
|
+
suffix += 1;
|
|
44
|
+
}
|
|
45
|
+
const removed = beforeLines.slice(prefix, beforeLines.length - suffix);
|
|
46
|
+
const added = afterLines.slice(prefix, afterLines.length - suffix);
|
|
47
|
+
lines.push(`@@ -${prefix + 1},${removed.length} +${prefix + 1},${added.length} @@`);
|
|
48
|
+
for (const line of removed)
|
|
49
|
+
lines.push(`-${line}`);
|
|
50
|
+
for (const line of added)
|
|
51
|
+
lines.push(`+${line}`);
|
|
52
|
+
return `${lines.join('\n')}\n`;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Builds the strict persisted InitPlan plus the exact bytes needed by apply.
|
|
56
|
+
* Plan JSON never includes file contents; diffs stay in the runtime bundle.
|
|
57
|
+
*/
|
|
58
|
+
export function buildInitPlan(input) {
|
|
59
|
+
const writes = [];
|
|
60
|
+
const files = input.files.map((file) => {
|
|
61
|
+
if (!isPortableRelativePath(file.path)) {
|
|
62
|
+
throw new Error(`Planned file path is not workspace-relative: ${file.path}`);
|
|
63
|
+
}
|
|
64
|
+
const operation = file.operation ?? 'create';
|
|
65
|
+
const expectedBeforeDigest = file.expectedBeforeDigest ?? null;
|
|
66
|
+
const digest = sha256Digest(file.bytes);
|
|
67
|
+
writes.push({
|
|
68
|
+
path: file.path,
|
|
69
|
+
operation,
|
|
70
|
+
bytes: file.bytes,
|
|
71
|
+
digest,
|
|
72
|
+
diff: unifiedFileDiff(file.path, file.beforeText ?? null, file.bytes.toString('utf8')),
|
|
73
|
+
});
|
|
74
|
+
return {
|
|
75
|
+
path: file.path,
|
|
76
|
+
operation,
|
|
77
|
+
expectedBeforeDigest,
|
|
78
|
+
nextDigest: digest,
|
|
79
|
+
backupRequired: file.backupRequired ?? operation !== 'create',
|
|
80
|
+
owner: 'pappra',
|
|
81
|
+
};
|
|
82
|
+
});
|
|
83
|
+
const plan = {
|
|
84
|
+
schema: 'pappra/init-plan/1',
|
|
85
|
+
planId: input.planId ?? randomUUID(),
|
|
86
|
+
workspaceRoot: input.workspaceRoot,
|
|
87
|
+
discoveredAt: input.discoveredAt ?? new Date().toISOString(),
|
|
88
|
+
configurationDigest: input.configurationDigest,
|
|
89
|
+
files,
|
|
90
|
+
externalActions: [...(input.externalActions ?? [])],
|
|
91
|
+
};
|
|
92
|
+
const parsed = initPlanSchema.safeParse(plan);
|
|
93
|
+
if (!parsed.success) {
|
|
94
|
+
throw new Error(`Internal init plan is invalid: ${parsed.error.issues[0]?.message ?? 'unknown issue'}`);
|
|
95
|
+
}
|
|
96
|
+
return { plan: parsed.data, writes };
|
|
97
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Diagnostic } from '../schemas/diagnostics.js';
|
|
2
|
+
export type WorkspaceRemovalStatus = 'rolled-back' | 'uninstalled' | 'partial' | 'missing' | 'unmanaged' | 'locked' | 'failed';
|
|
3
|
+
export interface WorkspaceRemovalResult {
|
|
4
|
+
readonly ok: boolean;
|
|
5
|
+
readonly status: WorkspaceRemovalStatus;
|
|
6
|
+
readonly removedPaths: readonly string[];
|
|
7
|
+
readonly residualPaths: readonly string[];
|
|
8
|
+
readonly diagnostics: readonly Diagnostic[];
|
|
9
|
+
}
|
|
10
|
+
export interface RollbackWorkspaceOptions {
|
|
11
|
+
readonly root: string;
|
|
12
|
+
readonly installId: string;
|
|
13
|
+
}
|
|
14
|
+
export declare function rollbackWorkspaceInstall(options: RollbackWorkspaceOptions): Promise<WorkspaceRemovalResult>;
|
|
15
|
+
export interface UninstallWorkspaceOptions {
|
|
16
|
+
readonly root: string;
|
|
17
|
+
}
|
|
18
|
+
export declare function uninstallWorkspaceManagedFiles(options: UninstallWorkspaceOptions): Promise<WorkspaceRemovalResult>;
|