@papillon6814/copi 0.1.0 → 0.2.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/LICENSE.unicode +39 -0
- package/README.md +57 -5
- package/dist/native/rename-exclusive-arm64 +0 -0
- package/dist/native/rename-exclusive-arm64.manifest.json +12 -0
- package/dist/src/apply.d.ts +9 -0
- package/dist/src/apply.js +110 -9
- package/dist/src/catalog.d.ts +21 -4
- package/dist/src/catalog.js +265 -133
- package/dist/src/cli.d.ts +2 -1
- package/dist/src/cli.js +73 -3
- package/dist/src/groups.js +8 -3
- package/dist/src/inspect.d.ts +3 -1
- package/dist/src/inspect.js +16 -6
- package/dist/src/instruction-reference.d.ts +6 -0
- package/dist/src/instruction-reference.js +21 -0
- package/dist/src/manage.d.ts +2 -0
- package/dist/src/manage.js +329 -0
- package/dist/src/migrate.d.ts +25 -0
- package/dist/src/migrate.js +171 -0
- package/dist/src/migration-backup.d.ts +22 -0
- package/dist/src/migration-backup.js +403 -0
- package/dist/src/migration-inventory.d.ts +9 -0
- package/dist/src/migration-inventory.js +237 -0
- package/dist/src/migration-resolution.d.ts +28 -0
- package/dist/src/migration-resolution.js +1618 -0
- package/dist/src/migration.d.ts +49 -0
- package/dist/src/migration.js +1004 -0
- package/dist/src/names.d.ts +11 -0
- package/dist/src/names.js +40 -0
- package/dist/src/native-placement.d.ts +16 -0
- package/dist/src/native-placement.js +557 -0
- package/dist/src/refresh.d.ts +20 -0
- package/dist/src/refresh.js +143 -0
- package/dist/src/resource-mutations.d.ts +45 -0
- package/dist/src/resource-mutations.js +225 -0
- package/dist/src/source-content.d.ts +4 -0
- package/dist/src/source-content.js +208 -0
- package/dist/src/storage-journal.d.ts +111 -0
- package/dist/src/storage-journal.js +613 -0
- package/dist/src/storage-root-lease.d.ts +7 -0
- package/dist/src/storage-root-lease.js +92 -0
- package/dist/src/storage-transaction.d.ts +100 -0
- package/dist/src/storage-transaction.js +3951 -0
- package/dist/src/terminal-editor.d.ts +8 -0
- package/dist/src/terminal-editor.js +391 -0
- package/dist/src/types.d.ts +18 -2
- package/dist/src/unicode-case-folding-data.d.ts +1 -0
- package/dist/src/unicode-case-folding-data.js +1596 -0
- package/dist/src/unicode-case-folding.d.ts +2 -0
- package/dist/src/unicode-case-folding.js +9 -0
- package/dist/src/wizard.d.ts +12 -3
- package/dist/src/wizard.js +287 -19
- package/package.json +5 -3
package/dist/src/groups.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { assertNameAvailable, assertNoNameConflicts, nameKey } from "./names.js";
|
|
2
3
|
function findGroup(catalog, groupId) {
|
|
3
4
|
const group = catalog.groups.find((item) => item.id === groupId);
|
|
4
5
|
if (!group)
|
|
@@ -6,20 +7,24 @@ function findGroup(catalog, groupId) {
|
|
|
6
7
|
return group;
|
|
7
8
|
}
|
|
8
9
|
function normalizedName(name) {
|
|
9
|
-
|
|
10
|
-
throw new Error("Group name must not be empty.");
|
|
10
|
+
nameKey(name);
|
|
11
11
|
return name.trim();
|
|
12
12
|
}
|
|
13
13
|
export function createGroup(catalog, name) {
|
|
14
|
+
assertNoNameConflicts(catalog);
|
|
15
|
+
assertNameAvailable(catalog, "group", name);
|
|
14
16
|
const group = { id: randomUUID(), name: normalizedName(name), itemIds: [] };
|
|
15
17
|
catalog.groups.push(group);
|
|
16
18
|
return group;
|
|
17
19
|
}
|
|
18
20
|
export function renameGroup(catalog, groupId, name) {
|
|
21
|
+
const group = findGroup(catalog, groupId);
|
|
19
22
|
const nextName = normalizedName(name);
|
|
20
|
-
|
|
23
|
+
assertNameAvailable(catalog, "group", nextName, groupId);
|
|
24
|
+
group.name = nextName;
|
|
21
25
|
}
|
|
22
26
|
export function setGroupItems(catalog, groupId, itemIds) {
|
|
27
|
+
assertNoNameConflicts(catalog);
|
|
23
28
|
const group = findGroup(catalog, groupId);
|
|
24
29
|
if (!Array.isArray(itemIds) || new Set(itemIds).size !== itemIds.length)
|
|
25
30
|
throw new Error("Group membership must contain unique item identifiers.");
|
package/dist/src/inspect.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { type PreparedResource, type PreparedInspection } from "./catalog.ts";
|
|
1
|
+
import { type PreparedResource, type PreparedInspection, type PreparedResourceContents } from "./catalog.ts";
|
|
2
2
|
import type { InspectionResult, ResourceCandidate } from "./types.ts";
|
|
3
|
+
export declare function inspectFixedResource({ candidate, files, excludedPaths }: PreparedResourceContents): InspectionResult;
|
|
3
4
|
export declare function inspectPreparedResource(prepared: PreparedResource): Promise<PreparedInspection>;
|
|
5
|
+
export declare function assertPreparedInspection(inspection: PreparedInspection, prepared: PreparedResource): void;
|
|
4
6
|
export declare function inspectResource(candidate: ResourceCandidate): Promise<InspectionResult>;
|
|
5
7
|
export declare function maskSensitiveText(value: string): string;
|
package/dist/src/inspect.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { prepareResource, readPreparedResource } from "./catalog.js";
|
|
3
|
+
const inspections = new WeakSet();
|
|
3
4
|
const maxInspectionBytes = 1_000_000;
|
|
4
5
|
const relativePathReference = /(?:^|[\s"'`])(?:\.{1,2}\/)?[A-Za-z0-9_.@-]+(?:\/[A-Za-z0-9_.@-]+)+(?:$|[\s"'`),;!?])/m;
|
|
5
6
|
function addWarning(warnings, code, message, relativePath) {
|
|
@@ -13,8 +14,7 @@ function containsInlineSecret(text) {
|
|
|
13
14
|
const bearer = /\bBearer\s+[A-Za-z0-9._~+/=-]{12,}/i;
|
|
14
15
|
return namedValue.test(text) || recognizableToken.test(text) || bearer.test(text);
|
|
15
16
|
}
|
|
16
|
-
export
|
|
17
|
-
const { candidate, files, excludedPaths } = readPreparedResource(prepared);
|
|
17
|
+
export function inspectFixedResource({ candidate, files, excludedPaths }) {
|
|
18
18
|
const warnings = [];
|
|
19
19
|
const sensitivePaths = [];
|
|
20
20
|
addWarning(warnings, "secret-detection-limited", "Secret detection is heuristic and cannot guarantee completeness.");
|
|
@@ -64,12 +64,22 @@ export async function inspectPreparedResource(prepared) {
|
|
|
64
64
|
}
|
|
65
65
|
if (candidate.destination.kind === "global-only")
|
|
66
66
|
addWarning(warnings, "global-only", "This resource is saved for reference and cannot be applied to a project.");
|
|
67
|
-
return
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
67
|
+
return { warnings, sensitivePaths: [...new Set(sensitivePaths)].sort((left, right) => left.localeCompare(right)), excludedPaths: [...new Set(excludedPaths)].sort((left, right) => left.localeCompare(right)) };
|
|
68
|
+
}
|
|
69
|
+
export async function inspectPreparedResource(prepared) {
|
|
70
|
+
const result = inspectFixedResource(readPreparedResource(prepared));
|
|
71
|
+
const inspection = Object.freeze({
|
|
72
|
+
warnings: Object.freeze(result.warnings.map((warning) => Object.freeze({ ...warning }))),
|
|
73
|
+
sensitivePaths: Object.freeze(result.sensitivePaths),
|
|
74
|
+
excludedPaths: Object.freeze(result.excludedPaths),
|
|
71
75
|
preparedResource: prepared,
|
|
72
76
|
});
|
|
77
|
+
inspections.add(inspection);
|
|
78
|
+
return inspection;
|
|
79
|
+
}
|
|
80
|
+
export function assertPreparedInspection(inspection, prepared) {
|
|
81
|
+
if (!inspections.has(inspection) || inspection.preparedResource !== prepared)
|
|
82
|
+
throw new Error("Inspection does not match the prepared resource or is not an authentic inspection.");
|
|
73
83
|
}
|
|
74
84
|
export async function inspectResource(candidate) {
|
|
75
85
|
const prepared = await prepareResource(candidate);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare const contextFileNames: readonly string[];
|
|
2
|
+
export declare function isAutoLoadedInstruction(destination: string): boolean;
|
|
3
|
+
export declare function referenceLine(instructionPath: string): string;
|
|
4
|
+
export declare function unreferencedPaths(instructionPaths: readonly string[], contextTexts: readonly string[]): string[];
|
|
5
|
+
export declare function chooseReferenceTarget(existingNames: readonly string[]): string;
|
|
6
|
+
export declare function appendReferenceLines(existing: string | undefined, instructionPaths: readonly string[]): string;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
export const contextFileNames = ["AGENTS.override.md", "AGENTS.md", "AGENTS.MD", "CLAUDE.md", "CLAUDE.MD"];
|
|
3
|
+
const systemPromptPaths = new Set([".pi/SYSTEM.md", ".pi/APPEND_SYSTEM.md"]);
|
|
4
|
+
export function isAutoLoadedInstruction(destination) {
|
|
5
|
+
return contextFileNames.includes(path.posix.basename(destination)) || systemPromptPaths.has(destination);
|
|
6
|
+
}
|
|
7
|
+
export function referenceLine(instructionPath) {
|
|
8
|
+
return `- 作業を始める前に \`${instructionPath}\` を読み、その指示に従うこと。`;
|
|
9
|
+
}
|
|
10
|
+
export function unreferencedPaths(instructionPaths, contextTexts) {
|
|
11
|
+
return [...new Set(instructionPaths)].filter((value) => !contextTexts.some((text) => text.includes(value)));
|
|
12
|
+
}
|
|
13
|
+
export function chooseReferenceTarget(existingNames) {
|
|
14
|
+
return contextFileNames.find((name) => existingNames.includes(name)) ?? "AGENTS.md";
|
|
15
|
+
}
|
|
16
|
+
export function appendReferenceLines(existing, instructionPaths) {
|
|
17
|
+
const lines = `${instructionPaths.map(referenceLine).join("\n")}\n`;
|
|
18
|
+
if (!existing)
|
|
19
|
+
return lines;
|
|
20
|
+
return `${existing.endsWith("\n") ? existing : `${existing}\n`}\n${lines}`;
|
|
21
|
+
}
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import { confirm, intro, isCancel, note, outro, select, text } from "@clack/prompts";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { lstat } from "node:fs/promises";
|
|
4
|
+
import { assertModernCatalog, loadCatalog, readSnapshotFiles, saveCatalog, saveNameChanges } from "./catalog.js";
|
|
5
|
+
import { assertNameAvailable, assertNoNameConflicts, findNameConflicts } from "./names.js";
|
|
6
|
+
import { commitSavedChange, prepareSavedChange, prepareSavedEdit, readSavedChange, readSavedEdit } from "./resource-mutations.js";
|
|
7
|
+
import { editTerminalText } from "./terminal-editor.js";
|
|
8
|
+
import { commitRefresh, discoverRefreshOrigin, prepareRefresh, readRefresh, resolveRefreshPath } from "./refresh.js";
|
|
9
|
+
import { resolveNameConflicts, runApplyWizard, runGroupManager, runSetup, safeDisplay, withEditorInput, withWizardLifecycle } from "./wizard.js";
|
|
10
|
+
class Cancelled extends Error {
|
|
11
|
+
}
|
|
12
|
+
function checked(value, signal) {
|
|
13
|
+
if (isCancel(value) || signal.aborted)
|
|
14
|
+
throw new Cancelled();
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
async function choose(message, options, signal) {
|
|
18
|
+
return checked(await select({ message, signal, options: options.map(option => ({ ...option, label: safeDisplay(option.label), ...(option.hint === undefined ? {} : { hint: safeDisplay(option.hint) }) })) }), signal);
|
|
19
|
+
}
|
|
20
|
+
async function yes(message, signal) {
|
|
21
|
+
return checked(await confirm({ message: safeDisplay(message), initialValue: false, signal }), signal) === true;
|
|
22
|
+
}
|
|
23
|
+
function show(body, title) { note(safeDisplay(body), safeDisplay(title)); }
|
|
24
|
+
function hint(catalog, item, order) {
|
|
25
|
+
const groups = catalog.groups.filter(group => group.itemIds.includes(item.id)).map(group => group.name).join(", ") || "no groups";
|
|
26
|
+
return `${groups} · ${item.destination.kind === "project" ? item.destination.path : "saved only"} · ${item.source} · saved ${item.savedAt} · entry ${order + 1}`;
|
|
27
|
+
}
|
|
28
|
+
async function savedFile(item, signal) {
|
|
29
|
+
if (item.files.length === 0) {
|
|
30
|
+
show("No registered saved files.", item.name);
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
return choose("Select saved file", item.files.map(file => ({ value: file.path, label: file.path })), signal);
|
|
34
|
+
}
|
|
35
|
+
async function view(libraryDir, item, signal) {
|
|
36
|
+
const filePath = await savedFile(item, signal);
|
|
37
|
+
if (filePath === undefined)
|
|
38
|
+
return;
|
|
39
|
+
const file = (await readSnapshotFiles(libraryDir, item)).find(file => file.path === filePath);
|
|
40
|
+
if (!file)
|
|
41
|
+
throw new Error("Selected saved file is no longer registered; reload the library.");
|
|
42
|
+
if (file.bytes.length > 1_000_000 || file.bytes.includes(0)) {
|
|
43
|
+
show(`${file.path}: [binary or large file, ${file.bytes.length} bytes; content not displayed]`, "Masked saved view");
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
const content = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(file.bytes);
|
|
48
|
+
// Full text, not a silently truncated preview. Display safety never alters stored bytes.
|
|
49
|
+
process.stdout.write(`\nMasked saved view: ${safeDisplay(item.name)} · ${safeDisplay(file.path)}\n${safeDisplay(content)}\n`);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
show(`${file.path}: [unsupported encoding, ${file.bytes.length} bytes; content not displayed]`, "Masked saved view");
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
async function edit(libraryDir, item, signal) {
|
|
56
|
+
const filePath = await savedFile(item, signal);
|
|
57
|
+
if (filePath === undefined)
|
|
58
|
+
return;
|
|
59
|
+
const prepared = await prepareSavedEdit(libraryDir, item.id, filePath);
|
|
60
|
+
const original = readSavedEdit(prepared);
|
|
61
|
+
show(original.warnings.map(w => `${w.path ? `${w.path}: ` : ""}${w.message}`).join("\n"), "Editing warnings · original files will not be changed");
|
|
62
|
+
if (original.sensitivePaths.includes(filePath) && !await yes(`Expose raw text of ${filePath} from ${item.name} in the editor? Suspected secrets may be exposed on the screen, scrollback or terminal recordings. Detection is heuristic. This individual approval is not approval to save.`, signal)) {
|
|
63
|
+
outro("Raw text was not exposed; nothing was saved.");
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (signal.aborted)
|
|
67
|
+
throw new Cancelled();
|
|
68
|
+
const initial = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(original.file.bytes);
|
|
69
|
+
const result = await withEditorInput(() => editTerminalText(initial));
|
|
70
|
+
if (result.kind === "cancelled" || signal.aborted)
|
|
71
|
+
throw new Cancelled();
|
|
72
|
+
const change = await prepareSavedChange(prepared, Buffer.from(result.text, "utf8"));
|
|
73
|
+
const review = readSavedChange(change);
|
|
74
|
+
if (!review.bytesChanged) {
|
|
75
|
+
outro("No byte changes; nothing was saved.");
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
process.stdout.write(`\n${safeDisplay(`Full masked saved diff: ${item.name}`)}\n`);
|
|
79
|
+
for (const diff of review.diffs)
|
|
80
|
+
process.stdout.write(`${safeDisplay(diff.path)}\n${safeDisplay(diff.summary)}\n${safeDisplay(diff.diff)}\n`);
|
|
81
|
+
show(review.warnings.map(w => `${w.path ? `${w.path}: ` : ""}${w.message}`).join("\n"), "Save warnings · masked text can hide byte changes");
|
|
82
|
+
if (!await yes("Approve this masked diff for saving? All hunks and byte summaries are shown above.", signal)) {
|
|
83
|
+
outro("Diff was not approved; nothing was saved.");
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const approvedSensitivePaths = [];
|
|
87
|
+
for (const file of review.sensitivePaths) {
|
|
88
|
+
if (!await yes(`Save suspected sensitive file ${file} from ${item.name}? This is a new approval for the prepared edited bytes, separate from raw text exposure. Detection is heuristic.`, signal)) {
|
|
89
|
+
outro("Sensitive saved bytes were not approved; nothing was saved.");
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
approvedSensitivePaths.push(file);
|
|
93
|
+
}
|
|
94
|
+
if (!await yes(`Save edited copy of ${filePath}? Original files stay unchanged; no history or restore is provided.`, signal)) {
|
|
95
|
+
outro("Nothing was saved.");
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (signal.aborted)
|
|
99
|
+
throw new Cancelled();
|
|
100
|
+
await commitSavedChange(change, { reviewId: change.reviewId, confirmed: true, approvedSensitivePaths });
|
|
101
|
+
outro("Edited saved copy committed with matching checksums.");
|
|
102
|
+
}
|
|
103
|
+
async function rename(libraryDir, catalog, item, signal) {
|
|
104
|
+
const name = checked(await text({ message: `New display name for ${safeDisplay(item.name)}`, signal, validate: value => {
|
|
105
|
+
try {
|
|
106
|
+
assertNameAvailable(catalog, "item", value ?? "", item.id);
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
return safeDisplay(error instanceof Error ? error.message : "Choose a unique non-empty name.");
|
|
111
|
+
}
|
|
112
|
+
} }), signal);
|
|
113
|
+
show(`${item.name} → ${name}\nSaved contents, file names, group references and destination remain unchanged.`, "Display name change");
|
|
114
|
+
if (await yes("Save display name only?", signal)) {
|
|
115
|
+
const after = structuredClone(catalog);
|
|
116
|
+
after.items.find(entry => entry.id === item.id).name = name.trim();
|
|
117
|
+
// Keep the UI's original object/revision; never reload and overwrite a newer name.
|
|
118
|
+
await saveNameChanges(libraryDir, catalog, after);
|
|
119
|
+
outro("Display name saved; contents and destination unchanged.");
|
|
120
|
+
}
|
|
121
|
+
else
|
|
122
|
+
outro("Display name was not changed.");
|
|
123
|
+
}
|
|
124
|
+
async function placement(libraryDir, catalog, item, signal) {
|
|
125
|
+
const kind = await choose("Destination kind", [
|
|
126
|
+
...(item.destination.kind === "project" ? [{ value: "project", label: "Project-relative path" }] : []),
|
|
127
|
+
{ value: "global-only", label: "Saved only (cannot later become project-applicable)" },
|
|
128
|
+
], signal);
|
|
129
|
+
let destination = { kind: "global-only" };
|
|
130
|
+
if (kind === "project") {
|
|
131
|
+
const destinationPath = checked(await text({ message: "New project-relative destination (separate from display name)", signal, validate: value => value?.trim() ? undefined : "Enter a project-relative path." }), signal);
|
|
132
|
+
destination = { kind: "project", path: destinationPath };
|
|
133
|
+
}
|
|
134
|
+
show(`${item.destination.kind === "project" ? item.destination.path : "saved only"} → ${destination.kind === "project" ? destination.path : "saved only"}\nDisplay name and saved contents remain unchanged.`, "Destination change");
|
|
135
|
+
if (await yes("Save destination only?", signal)) {
|
|
136
|
+
item.destination = destination;
|
|
137
|
+
// saveCatalog verifies this original loadCatalog object's revision under its commit lock.
|
|
138
|
+
await saveCatalog(libraryDir, catalog);
|
|
139
|
+
outro("Destination saved; display name and contents unchanged.");
|
|
140
|
+
}
|
|
141
|
+
else
|
|
142
|
+
outro("Destination was not changed.");
|
|
143
|
+
}
|
|
144
|
+
async function refresh(libraryDir, catalog, item, roots, signal) {
|
|
145
|
+
show("sourceRoot was not stored in the saved catalog. No HOME or parent tree will be searched implicitly. Select only a same-kind source; a skill uses a directory tree, other kinds use one regular file. Private/auth/.env*/cache/history paths remain excluded.", "Refresh source boundary");
|
|
146
|
+
let requested = item.sourcePath;
|
|
147
|
+
let exists;
|
|
148
|
+
try {
|
|
149
|
+
await lstat(requested);
|
|
150
|
+
exists = true;
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
if (error.code !== "ENOENT")
|
|
154
|
+
throw error;
|
|
155
|
+
exists = false;
|
|
156
|
+
}
|
|
157
|
+
let source;
|
|
158
|
+
if (exists && (item.source.startsWith("package:") || requested.split(path.sep).some(part => part.toLowerCase() === "node_modules"))) {
|
|
159
|
+
const route = await choose("Registered package source", [{ value: "discover", label: "Rediscover declared origin in explicitly permitted locations" }, { value: "manual", label: "Choose a different non-excluded source path" }], signal);
|
|
160
|
+
if (route === "discover") {
|
|
161
|
+
const scopes = await choose("Select origin search scope", [{ value: "project", label: `Current project: ${roots.projectDir}` }, { value: "home", label: `HOME and Pi agent directory: ${roots.homeDir} · ${roots.agentDir}` }, { value: "both", label: "HOME, Pi agent directory and current project" }], signal);
|
|
162
|
+
const home = scopes !== "project", project = scopes !== "home";
|
|
163
|
+
const options = { ...roots, scopes: { home, project } };
|
|
164
|
+
if (home) {
|
|
165
|
+
options.homeDir = await resolveRefreshPath(roots.homeDir);
|
|
166
|
+
options.agentDir = await resolveRefreshPath(roots.agentDir);
|
|
167
|
+
}
|
|
168
|
+
if (project)
|
|
169
|
+
options.projectDir = await resolveRefreshPath(roots.projectDir);
|
|
170
|
+
show(`Original path: ${requested}\nHOME: ${home ? roots.homeDir : "not selected"}\nPhysical HOME: ${home ? options.homeDir : "not selected"}\nPi agent: ${home ? roots.agentDir : "not selected"}\nPhysical Pi agent: ${home ? options.agentDir : "not selected"}\nProject: ${project ? roots.projectDir : "not selected"}\nPhysical project: ${project ? options.projectDir : "not selected"}\nDiscovery uses existing declarations, exclusions and package checks; no arbitrary node_modules exception is granted.`, "Explicit origin discovery");
|
|
171
|
+
if (!await yes("Authorize origin discovery only in these selected locations?", signal)) {
|
|
172
|
+
outro("Origin discovery not authorized; nothing was saved.");
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
source = await discoverRefreshOrigin(item, options);
|
|
176
|
+
if (!source)
|
|
177
|
+
show("Declared origin is no longer available in the approved locations. Choose another non-excluded source explicitly, or cancel; the saved copy remains available.", "Origin unavailable");
|
|
178
|
+
}
|
|
179
|
+
exists = Boolean(source);
|
|
180
|
+
}
|
|
181
|
+
if (!exists) {
|
|
182
|
+
show("The original source is unavailable or was not selected. The old saved copy can still be viewed. Choose a new source path explicitly, or Esc / Ctrl-C to cancel.", "Source selection");
|
|
183
|
+
requested = checked(await text({ message: "New source path (absolute; same resource kind)", signal, validate: value => value && path.isAbsolute(value) ? undefined : "Enter an absolute source path." }), signal);
|
|
184
|
+
const scope = await choose("Source scope (metadata only; does not authorize wider searches)", [{ value: "project", label: "Project" }, { value: "user", label: "User" }, { value: "shared", label: "Shared" }, { value: "package", label: "Package (no private path exception)" }], signal);
|
|
185
|
+
const physical = await resolveRefreshPath(requested);
|
|
186
|
+
source = { id: item.id, kind: item.kind, name: item.name, source: "explicit-refresh-path", scope, sourcePath: physical, sourceRoot: path.dirname(physical), destination: { ...item.destination } };
|
|
187
|
+
}
|
|
188
|
+
if (!source) {
|
|
189
|
+
const physical = await resolveRefreshPath(requested);
|
|
190
|
+
source = { id: item.id, kind: item.kind, name: item.name, source: item.source, scope: item.scope, sourcePath: physical, sourceRoot: path.dirname(physical), destination: { ...item.destination } };
|
|
191
|
+
}
|
|
192
|
+
show(`Requested path: ${requested}\nPhysical source: ${source.sourcePath}\nValidation boundary: ${source.sourceRoot}\nRead only: ${item.kind === "skill" ? "selected directory tree" : "selected regular file"}\nKind: ${source.kind}; source: ${source.source}; scope: ${source.scope}\nNo parent-tree search. Existing exclusions and symlink checks apply.`, "Source read permission");
|
|
193
|
+
if (!await yes("Authorize reading only this selected source file or directory tree now?", signal)) {
|
|
194
|
+
outro("Source reading not authorized; nothing was saved.");
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
if (signal.aborted)
|
|
198
|
+
throw new Cancelled();
|
|
199
|
+
const prepared = await prepareRefresh(libraryDir, item.id, source), review = readRefresh(prepared);
|
|
200
|
+
if (JSON.stringify(review.originalCatalog) !== JSON.stringify(catalog))
|
|
201
|
+
throw new Error("Saved metadata, memberships or copy state changed since selection; reload and review again.");
|
|
202
|
+
const original = review.originalCatalog.items.find(entry => entry.id === item.id);
|
|
203
|
+
show(`${original.name} · ${hint(review.originalCatalog, original, review.originalCatalog.items.indexOf(original))}\nSource: ${original.sourcePath} → ${review.item.sourcePath}\nSource metadata: ${original.source} / ${original.scope} → ${review.item.source} / ${review.item.scope}\nID, display name, all memberships, savedAt and destination are retained.`, "Prepared refresh identity and source");
|
|
204
|
+
process.stdout.write(`\n${safeDisplay(`Full masked refresh diff: ${review.item.name}`)}\n`);
|
|
205
|
+
for (const diff of review.diffs)
|
|
206
|
+
process.stdout.write(`${safeDisplay(diff.path)}\n${safeDisplay(diff.summary)}\n${safeDisplay(diff.diff)}\n`);
|
|
207
|
+
show(review.warnings.map(w => `${w.path ? `${w.path}: ` : ""}${w.message}`).join("\n"), "Fixed source inspection warnings · masking can hide byte changes");
|
|
208
|
+
if (review.excludedPaths.length)
|
|
209
|
+
show(review.excludedPaths.join("\n"), "Fixed source excluded paths");
|
|
210
|
+
show("All copi-only edits will be lost when this source snapshot replaces the saved copy, including added/removed bundle files and mode changes shown above. Original files will not be changed or written back. There is no history, restore or automatic merge.", "Replacement warning");
|
|
211
|
+
if (!await yes("Approve this refresh diff and source metadata? All masked hunks and byte/mode summaries are shown above.", signal)) {
|
|
212
|
+
outro("Refresh diff not approved; nothing was saved.");
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
const approvedSensitivePaths = [];
|
|
216
|
+
for (const file of review.sensitivePaths) {
|
|
217
|
+
if (!await yes(`Save suspected sensitive file ${file} from ${review.item.name}? This individual approval covers only the fixed prepared source bytes. Detection is heuristic; raw source is not displayed.`, signal)) {
|
|
218
|
+
outro("Sensitive source bytes not approved; nothing was saved.");
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
approvedSensitivePaths.push(file);
|
|
222
|
+
}
|
|
223
|
+
if (!await yes("Replace saved copy with this approved source snapshot? Copi edits will be lost; originals stay unchanged; no history, restore or automatic merge.", signal)) {
|
|
224
|
+
outro("Refresh cancelled; nothing was saved.");
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
if (signal.aborted)
|
|
228
|
+
throw new Cancelled();
|
|
229
|
+
await commitRefresh(prepared, { reviewId: prepared.reviewId, confirmed: true, approvedSensitivePaths });
|
|
230
|
+
outro("Refreshed saved copy committed with matching checksums; originals unchanged.");
|
|
231
|
+
}
|
|
232
|
+
async function resource(libraryDir, catalog, conflict, roots, signal) {
|
|
233
|
+
const id = await choose("Select saved resource", catalog.items.map((item, index) => ({ value: item.id, label: `[${item.kind}] ${item.name}`, hint: hint(catalog, item, index) })), signal);
|
|
234
|
+
if (catalog.version === 1) {
|
|
235
|
+
const item = catalog.items.find(item => item.id === id);
|
|
236
|
+
if (!item)
|
|
237
|
+
throw new Error("Selected resource is unavailable; reload the library.");
|
|
238
|
+
const action = await choose("Resource action", [{ value: "view", label: "View saved file (masked)" }, { value: "back", label: "Back" }], signal);
|
|
239
|
+
if (action === "view")
|
|
240
|
+
await view(libraryDir, item, signal);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
const item = catalog.items.find(item => item.id === id);
|
|
244
|
+
if (!item)
|
|
245
|
+
throw new Error("Selected resource is unavailable; reload the library.");
|
|
246
|
+
const action = await choose("Resource action", [
|
|
247
|
+
{ value: "view", label: "View saved file (masked)" },
|
|
248
|
+
...(!conflict ? [
|
|
249
|
+
{ value: "edit", label: "Edit saved text" }, { value: "rename", label: "Rename display name" }, { value: "placement", label: "Change destination" },
|
|
250
|
+
{ value: "refresh", label: "Refresh from source (replace saved copy)" },
|
|
251
|
+
] : []), { value: "back", label: "Back" },
|
|
252
|
+
], signal);
|
|
253
|
+
if (action === "view") {
|
|
254
|
+
await view(libraryDir, item, signal);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
if (action === "back")
|
|
258
|
+
return;
|
|
259
|
+
const current = await loadCatalog(libraryDir);
|
|
260
|
+
assertModernCatalog(current);
|
|
261
|
+
assertNoNameConflicts(current);
|
|
262
|
+
if (action === "edit")
|
|
263
|
+
await edit(libraryDir, item, signal);
|
|
264
|
+
else if (action === "rename")
|
|
265
|
+
await rename(libraryDir, catalog, item, signal);
|
|
266
|
+
else if (action === "refresh")
|
|
267
|
+
await refresh(libraryDir, catalog, item, roots, signal);
|
|
268
|
+
else
|
|
269
|
+
await placement(libraryDir, catalog, item, signal);
|
|
270
|
+
}
|
|
271
|
+
export async function runManage(libraryDir, roots) {
|
|
272
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY)
|
|
273
|
+
throw new Error("manage requires an interactive stdin/stdout TTY; no library was created or searched.");
|
|
274
|
+
await withWizardLifecycle(async (signal) => {
|
|
275
|
+
intro("copi manage · names, not IDs · Esc / Ctrl-C cancels");
|
|
276
|
+
try {
|
|
277
|
+
while (!signal.aborted) {
|
|
278
|
+
const catalog = await loadCatalog(libraryDir);
|
|
279
|
+
const modern = catalog.version === 2;
|
|
280
|
+
const conflict = findNameConflicts(catalog).length > 0;
|
|
281
|
+
if (conflict)
|
|
282
|
+
show(modern ? "Duplicate names: only listing, masked viewing, and explicit name conflict resolution are allowed." : "Legacy catalog v1 is read-only; duplicate names are not changed here. Only listing and masked viewing are available.", "Duplicate names");
|
|
283
|
+
else if (!modern)
|
|
284
|
+
show("Legacy catalog v1 is read-only until explicit migration.", "Read-only library");
|
|
285
|
+
else if (catalog.items.length === 0)
|
|
286
|
+
show("No saved resources. Setup searches only explicitly selected and approved locations.", "Private library");
|
|
287
|
+
const action = await choose("Manage saved resources", [
|
|
288
|
+
...(catalog.items.length ? [{ value: "resource", label: "Select resource by name" }] : []),
|
|
289
|
+
...(modern ? (conflict ? [{ value: "resolve", label: "Resolve name conflicts" }] : [
|
|
290
|
+
{ value: "groups", label: "Manage groups" }, { value: "apply", label: "Apply group" }, { value: "setup", label: "Setup copied resources" },
|
|
291
|
+
]) : []), { value: "done", label: "Finish" },
|
|
292
|
+
], signal);
|
|
293
|
+
if (action === "done")
|
|
294
|
+
break;
|
|
295
|
+
if (action === "resource")
|
|
296
|
+
await resource(libraryDir, catalog, conflict, roots, signal);
|
|
297
|
+
else if (action === "resolve")
|
|
298
|
+
await resolveNameConflicts(libraryDir);
|
|
299
|
+
else {
|
|
300
|
+
assertModernCatalog(catalog);
|
|
301
|
+
const current = await loadCatalog(libraryDir);
|
|
302
|
+
assertModernCatalog(current);
|
|
303
|
+
assertNoNameConflicts(current);
|
|
304
|
+
if (action === "groups")
|
|
305
|
+
await runGroupManager(libraryDir);
|
|
306
|
+
else if (action === "setup")
|
|
307
|
+
await runSetup(libraryDir, roots);
|
|
308
|
+
else {
|
|
309
|
+
if (catalog.groups.length === 0) {
|
|
310
|
+
show("No groups to apply. Create a group and assign resources first.", "Apply group");
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
const group = await choose("Select group to apply", catalog.groups.map(g => ({ value: g.id, label: g.name, hint: `${g.itemIds.length} resources` })), signal);
|
|
314
|
+
const target = checked(await text({ message: "Apply target directory", initialValue: roots.projectDir, signal }), signal);
|
|
315
|
+
await runApplyWizard(libraryDir, group, path.resolve(roots.projectDir, target));
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
outro("Manage finished.");
|
|
320
|
+
}
|
|
321
|
+
catch (error) {
|
|
322
|
+
if (error instanceof Cancelled || signal.aborted) {
|
|
323
|
+
outro("Manage cancelled; unconfirmed changes were not saved.");
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
throw new Error(safeDisplay(`${error instanceof Error ? error.message : "Management failed."}\nChanges stopped. Inspect the cause and reload before trying again; unknown locks and checksums are not automatically repaired.`), { cause: error });
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { commitMigration, prepareMigration, readMigration } from "./migration.ts";
|
|
2
|
+
import type { MigrationResolutionView, MigrationResult, PreparedMigrationResolution } from "./migration.ts";
|
|
3
|
+
import type { ChangeApproval } from "./resource-mutations.ts";
|
|
4
|
+
export interface MigrationCommandApi {
|
|
5
|
+
prepareMigration: typeof prepareMigration;
|
|
6
|
+
readMigration: typeof readMigration;
|
|
7
|
+
commitMigration: typeof commitMigration;
|
|
8
|
+
prepareMigrationResolution(libraryDir: string): Promise<PreparedMigrationResolution>;
|
|
9
|
+
readMigrationResolution(plan: PreparedMigrationResolution): MigrationResolutionView;
|
|
10
|
+
commitMigrationResolution(plan: PreparedMigrationResolution, approval: ChangeApproval): Promise<MigrationResult>;
|
|
11
|
+
}
|
|
12
|
+
export interface MigrationCommandRuntime {
|
|
13
|
+
api?: MigrationCommandApi;
|
|
14
|
+
stdinIsTTY?: boolean;
|
|
15
|
+
stdoutIsTTY?: boolean;
|
|
16
|
+
confirm?: (message: string, initialValue: false, signal: AbortSignal) => Promise<unknown>;
|
|
17
|
+
withLifecycle?: (operation: (signal: AbortSignal) => Promise<number>) => Promise<number>;
|
|
18
|
+
write?: (text: string) => void;
|
|
19
|
+
}
|
|
20
|
+
export interface MigrationCommandFlags {
|
|
21
|
+
readonly resolve: boolean;
|
|
22
|
+
readonly dryRun: boolean;
|
|
23
|
+
}
|
|
24
|
+
export declare function parseMigrationArgs(args: readonly string[]): MigrationCommandFlags;
|
|
25
|
+
export declare function runMigrateCommand(args: readonly string[], libraryDir: string, runtime?: MigrationCommandRuntime): Promise<number>;
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { confirm as clackConfirm } from "@clack/prompts";
|
|
2
|
+
import { commitMigration, prepareMigration, readMigration, } from "./migration.js";
|
|
3
|
+
import { safeDisplay, withWizardLifecycle } from "./wizard.js";
|
|
4
|
+
const usageError = () => new Error("Usage: copi migrate [--dry-run] | copi migrate --resolve [--dry-run].");
|
|
5
|
+
async function runWithWizardLifecycle(operation) {
|
|
6
|
+
let result = 1;
|
|
7
|
+
await withWizardLifecycle(async (signal) => { result = await operation(signal); });
|
|
8
|
+
return result;
|
|
9
|
+
}
|
|
10
|
+
export function parseMigrationArgs(args) {
|
|
11
|
+
let resolve = false;
|
|
12
|
+
let dryRun = false;
|
|
13
|
+
for (const argument of args) {
|
|
14
|
+
if (argument === "--resolve" && !resolve)
|
|
15
|
+
resolve = true;
|
|
16
|
+
else if (argument === "--dry-run" && !dryRun)
|
|
17
|
+
dryRun = true;
|
|
18
|
+
else
|
|
19
|
+
throw usageError();
|
|
20
|
+
}
|
|
21
|
+
return { resolve, dryRun };
|
|
22
|
+
}
|
|
23
|
+
function writeSafe(write, text) {
|
|
24
|
+
write(safeDisplay(text));
|
|
25
|
+
}
|
|
26
|
+
function displayReview(write, title, view) {
|
|
27
|
+
writeSafe(write, `${title}: ${view.status}\n`);
|
|
28
|
+
if ("backupDir" in view && view.backupDir)
|
|
29
|
+
writeSafe(write, `Verified backup: ${view.backupDir}\n`);
|
|
30
|
+
for (const item of view.items) {
|
|
31
|
+
writeSafe(write, `Resource: ${item.name} → ${item.target} (${item.selection})\n`);
|
|
32
|
+
}
|
|
33
|
+
for (const diff of view.maskedDiffs) {
|
|
34
|
+
writeSafe(write, `\n${diff.path}\n${diff.summary}\n`);
|
|
35
|
+
}
|
|
36
|
+
for (const warning of view.warnings) {
|
|
37
|
+
writeSafe(write, `Warning${warning.path ? ` (${warning.path})` : ""}: ${warning.message}\n`);
|
|
38
|
+
}
|
|
39
|
+
if (view.conflicts.length > 0)
|
|
40
|
+
writeSafe(write, `Conflicts:\n${view.conflicts.map((entry) => `- ${entry}`).join("\n")}\n`);
|
|
41
|
+
if (view.preservedPaths.length > 0)
|
|
42
|
+
writeSafe(write, `Preserved paths:\n${view.preservedPaths.map((entry) => `- ${entry}`).join("\n")}\n`);
|
|
43
|
+
}
|
|
44
|
+
function isMigrationLockConflict(view) {
|
|
45
|
+
return view.conflicts.some((conflict) => conflict.includes(".copi-transaction") || conflict.includes(".copi-migration-resolution.json") ||
|
|
46
|
+
conflict.includes(".copi-transaction-recovery"));
|
|
47
|
+
}
|
|
48
|
+
async function defaultApi() {
|
|
49
|
+
const module = await import("./migration.js");
|
|
50
|
+
return module;
|
|
51
|
+
}
|
|
52
|
+
function requireResolutionApi(api) {
|
|
53
|
+
if (typeof api.prepareMigrationResolution !== "function" ||
|
|
54
|
+
typeof api.readMigrationResolution !== "function" ||
|
|
55
|
+
typeof api.commitMigrationResolution !== "function") {
|
|
56
|
+
throw new Error("This build does not include the approved migration-resolution API.");
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
async function promptForApproval(view, resolve, confirm, signal) {
|
|
60
|
+
const approvedSensitivePaths = [];
|
|
61
|
+
const reviewQuestion = resolve
|
|
62
|
+
? "この masked diff をすべて確認し、解消手順の対象としてよいですか。"
|
|
63
|
+
: "この masked diff をすべて確認し、移行内容の対象としてよいですか。";
|
|
64
|
+
if (await confirm(reviewQuestion, false, signal) !== true || signal.aborted)
|
|
65
|
+
return undefined;
|
|
66
|
+
for (const sensitivePath of view.sensitivePaths) {
|
|
67
|
+
const question = `秘密情報候補 ${sensitivePath} を個別に確認しましたか。内容は表示されません。`;
|
|
68
|
+
if (await confirm(question, false, signal) !== true || signal.aborted)
|
|
69
|
+
return undefined;
|
|
70
|
+
approvedSensitivePaths.push(sensitivePath);
|
|
71
|
+
}
|
|
72
|
+
const finalQuestion = resolve
|
|
73
|
+
? "確認した sources の編集をそのまま保持し、移行終了を確定しますか。catalog・sources・items は書き換えません。検証済みバックアップを残し、所有を確認できる内部残物だけを片付けます。配布と items の削除は行いません。"
|
|
74
|
+
: "移行を実行しますか。catalog を v2 に切り替え、検証済みバックアップを保持します。";
|
|
75
|
+
if (await confirm(finalQuestion, false, signal) !== true || signal.aborted)
|
|
76
|
+
return undefined;
|
|
77
|
+
return { reviewId: view.reviewId, confirmed: true, approvedSensitivePaths };
|
|
78
|
+
}
|
|
79
|
+
function emitBlockedResolution(write, view) {
|
|
80
|
+
const baselineInsufficient = view.conflicts.some((conflict) => /v4|baseline-insufficient/i.test(conflict));
|
|
81
|
+
if (baselineInsufficient) {
|
|
82
|
+
writeSafe(write, "開始時 inventory が無いためこの解消手順では扱えません。従来の復旧/保持停止を維持します。\n");
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
writeSafe(write, "安全に解消できないため停止しました。編集と残物は保持しています。\n");
|
|
86
|
+
}
|
|
87
|
+
for (const conflict of view.conflicts)
|
|
88
|
+
writeSafe(write, `- ${conflict}\n`);
|
|
89
|
+
if (view.preservedPaths.length > 0) {
|
|
90
|
+
writeSafe(write, `残している path:\n${view.preservedPaths.map((entry) => `- ${entry}`).join("\n")}\n`);
|
|
91
|
+
}
|
|
92
|
+
writeSafe(write, "原因を確認し、新しい検証buildやfresh reviewが必要な場合はその結果を確認してから再実行してください。\n");
|
|
93
|
+
}
|
|
94
|
+
async function runResolution(api, libraryDir, flags, runtime) {
|
|
95
|
+
requireResolutionApi(api);
|
|
96
|
+
const plan = await api.prepareMigrationResolution(libraryDir);
|
|
97
|
+
const view = api.readMigrationResolution(plan);
|
|
98
|
+
displayReview(runtime.write, "Migration resolution", view);
|
|
99
|
+
if (view.status === "blocked") {
|
|
100
|
+
emitBlockedResolution(runtime.write, view);
|
|
101
|
+
return 1;
|
|
102
|
+
}
|
|
103
|
+
if (view.status === "not-needed") {
|
|
104
|
+
writeSafe(runtime.write, "解消が必要な移行残物はありません。\n");
|
|
105
|
+
return 0;
|
|
106
|
+
}
|
|
107
|
+
if (flags.dryRun)
|
|
108
|
+
return 0;
|
|
109
|
+
return runtime.withLifecycle(async (signal) => {
|
|
110
|
+
const approval = await promptForApproval(view, true, runtime.confirm, signal);
|
|
111
|
+
if (!approval || signal.aborted) {
|
|
112
|
+
writeSafe(runtime.write, "解消は確定されませんでした。編集と残物は保持しています。\n");
|
|
113
|
+
return 1;
|
|
114
|
+
}
|
|
115
|
+
const result = await api.commitMigrationResolution(plan, approval);
|
|
116
|
+
writeSafe(runtime.write, "確認付き解消が完了しました。catalog・sources・items は書き換えていません。\n");
|
|
117
|
+
if (result.backupDir)
|
|
118
|
+
writeSafe(runtime.write, `検証済みバックアップ: ${result.backupDir}\n`);
|
|
119
|
+
for (const preserved of result.preservedPaths)
|
|
120
|
+
writeSafe(runtime.write, `保持: ${preserved}\n`);
|
|
121
|
+
return 0;
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
async function runOrdinaryMigration(api, libraryDir, flags, runtime) {
|
|
125
|
+
const plan = await api.prepareMigration(libraryDir);
|
|
126
|
+
const view = api.readMigration(plan);
|
|
127
|
+
displayReview(runtime.write, "Legacy migration", view);
|
|
128
|
+
if (view.status === "blocked") {
|
|
129
|
+
if (isMigrationLockConflict(view)) {
|
|
130
|
+
writeSafe(runtime.write, "移行終了確認が未完了です。編集とバックアップを保持しています。`copi migrate --resolve --dry-run` で確認してください。内部 lock を手で削除しないでください。\n");
|
|
131
|
+
}
|
|
132
|
+
return 1;
|
|
133
|
+
}
|
|
134
|
+
if (view.status === "already-v2") {
|
|
135
|
+
writeSafe(runtime.write, "catalog はすでに v2 です。移行は必要ありません。\n");
|
|
136
|
+
return 0;
|
|
137
|
+
}
|
|
138
|
+
if (flags.dryRun)
|
|
139
|
+
return 0;
|
|
140
|
+
return runtime.withLifecycle(async (signal) => {
|
|
141
|
+
const approval = await promptForApproval(view, false, runtime.confirm, signal);
|
|
142
|
+
if (!approval || signal.aborted) {
|
|
143
|
+
writeSafe(runtime.write, "移行は確定されませんでした。library は変更していません。\n");
|
|
144
|
+
return 1;
|
|
145
|
+
}
|
|
146
|
+
const result = await api.commitMigration(plan, approval);
|
|
147
|
+
writeSafe(runtime.write, "移行が完了しました。\n");
|
|
148
|
+
if (result.backupDir)
|
|
149
|
+
writeSafe(runtime.write, `検証済みバックアップ: ${result.backupDir}\n`);
|
|
150
|
+
for (const preserved of result.preservedPaths)
|
|
151
|
+
writeSafe(runtime.write, `保持: ${preserved}\n`);
|
|
152
|
+
return 0;
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
export async function runMigrateCommand(args, libraryDir, runtime = {}) {
|
|
156
|
+
const flags = parseMigrationArgs(args);
|
|
157
|
+
const stdinIsTTY = runtime.stdinIsTTY ?? Boolean(process.stdin.isTTY);
|
|
158
|
+
const stdoutIsTTY = runtime.stdoutIsTTY ?? Boolean(process.stdout.isTTY);
|
|
159
|
+
const write = runtime.write ?? ((text) => process.stdout.write(text));
|
|
160
|
+
if (!flags.dryRun && (!stdinIsTTY || !stdoutIsTTY)) {
|
|
161
|
+
writeSafe(write, "migrate の変更には stdin/stdout の interactive TTY が必要です。--dry-run は read-only です。\n");
|
|
162
|
+
return 1;
|
|
163
|
+
}
|
|
164
|
+
const api = runtime.api ?? await defaultApi();
|
|
165
|
+
const confirm = runtime.confirm ?? (async (message, initialValue, signal) => clackConfirm({ message: safeDisplay(message), initialValue, signal }));
|
|
166
|
+
const withLifecycle = runtime.withLifecycle ?? runWithWizardLifecycle;
|
|
167
|
+
const dependencies = { confirm, withLifecycle, write };
|
|
168
|
+
return flags.resolve
|
|
169
|
+
? runResolution(api, libraryDir, flags, dependencies)
|
|
170
|
+
: runOrdinaryMigration(api, libraryDir, flags, dependencies);
|
|
171
|
+
}
|