@yoyoboot/yoyo-admin-cli 10.5.5
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/dist/bin.d.ts +9 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +10 -0
- package/dist/catalogs/capability.d.ts +54 -0
- package/dist/catalogs/capability.d.ts.map +1 -0
- package/dist/catalogs/capability.js +129 -0
- package/dist/catalogs/crud.d.ts +64 -0
- package/dist/catalogs/crud.d.ts.map +1 -0
- package/dist/catalogs/crud.js +83 -0
- package/dist/catalogs/source.d.ts +16 -0
- package/dist/catalogs/source.d.ts.map +1 -0
- package/dist/catalogs/source.js +8 -0
- package/dist/catalogs/standard-crud.d.ts +51 -0
- package/dist/catalogs/standard-crud.d.ts.map +1 -0
- package/dist/catalogs/standard-crud.js +100 -0
- package/dist/cli.d.ts +20 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +86 -0
- package/dist/commands/capabilities.d.ts +78 -0
- package/dist/commands/capabilities.d.ts.map +1 -0
- package/dist/commands/capabilities.js +72 -0
- package/dist/commands/contracts.d.ts +16 -0
- package/dist/commands/contracts.d.ts.map +1 -0
- package/dist/commands/contracts.js +83 -0
- package/dist/commands/create-frontend-module.d.ts +8 -0
- package/dist/commands/create-frontend-module.d.ts.map +1 -0
- package/dist/commands/create-frontend-module.js +90 -0
- package/dist/commands/create-package.d.ts +17 -0
- package/dist/commands/create-package.d.ts.map +1 -0
- package/dist/commands/create-package.js +75 -0
- package/dist/commands/create-registry.d.ts +32 -0
- package/dist/commands/create-registry.d.ts.map +1 -0
- package/dist/commands/create-registry.js +20 -0
- package/dist/commands/create.d.ts +23 -0
- package/dist/commands/create.d.ts.map +1 -0
- package/dist/commands/create.js +111 -0
- package/dist/commands/doctor.d.ts +17 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +150 -0
- package/dist/commands/inspect.d.ts +139 -0
- package/dist/commands/inspect.d.ts.map +1 -0
- package/dist/commands/inspect.js +379 -0
- package/dist/generators/frontend-module.d.ts +81 -0
- package/dist/generators/frontend-module.d.ts.map +1 -0
- package/dist/generators/frontend-module.js +358 -0
- package/dist/generators/package.d.ts +46 -0
- package/dist/generators/package.d.ts.map +1 -0
- package/dist/generators/package.js +230 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +24 -0
- package/dist/kernel/arguments.d.ts +44 -0
- package/dist/kernel/arguments.d.ts.map +1 -0
- package/dist/kernel/arguments.js +127 -0
- package/dist/kernel/command.d.ts +71 -0
- package/dist/kernel/command.d.ts.map +1 -0
- package/dist/kernel/command.js +120 -0
- package/dist/kernel/fs-plan.d.ts +50 -0
- package/dist/kernel/fs-plan.d.ts.map +1 -0
- package/dist/kernel/fs-plan.js +82 -0
- package/dist/kernel/help.d.ts +54 -0
- package/dist/kernel/help.d.ts.map +1 -0
- package/dist/kernel/help.js +109 -0
- package/dist/kernel/lockfile.d.ts +73 -0
- package/dist/kernel/lockfile.d.ts.map +1 -0
- package/dist/kernel/lockfile.js +117 -0
- package/dist/kernel/manifests-wiring.d.ts +44 -0
- package/dist/kernel/manifests-wiring.d.ts.map +1 -0
- package/dist/kernel/manifests-wiring.js +165 -0
- package/dist/kernel/plan-runner.d.ts +81 -0
- package/dist/kernel/plan-runner.d.ts.map +1 -0
- package/dist/kernel/plan-runner.js +176 -0
- package/dist/kernel/process-runner.d.ts +25 -0
- package/dist/kernel/process-runner.d.ts.map +1 -0
- package/dist/kernel/process-runner.js +29 -0
- package/dist/kernel/render.d.ts +20 -0
- package/dist/kernel/render.d.ts.map +1 -0
- package/dist/kernel/render.js +63 -0
- package/dist/kernel/result.d.ts +66 -0
- package/dist/kernel/result.d.ts.map +1 -0
- package/dist/kernel/result.js +36 -0
- package/dist/kernel/workspace.d.ts +31 -0
- package/dist/kernel/workspace.d.ts.map +1 -0
- package/dist/kernel/workspace.js +66 -0
- package/package.json +31 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `--help` 渲染。
|
|
3
|
+
*
|
|
4
|
+
* 铁律:**help 从 registry 投影,不维护第二份命令清单**。
|
|
5
|
+
* 根 help 遍历 `registry.list()`;`create` 的 recipe help 遍历
|
|
6
|
+
* `create-registry`。新增命令/recipe 后 help 自动跟随,无需改本文件。
|
|
7
|
+
*/
|
|
8
|
+
export function helpPayload(text) {
|
|
9
|
+
return { kind: 'help', text };
|
|
10
|
+
}
|
|
11
|
+
export function isHelpPayload(value) {
|
|
12
|
+
if (typeof value !== 'object' || value === null)
|
|
13
|
+
return false;
|
|
14
|
+
const candidate = value;
|
|
15
|
+
return candidate.kind === 'help' && typeof candidate.text === 'string';
|
|
16
|
+
}
|
|
17
|
+
/** 全局选项说明:与 `arguments.ts` 的全局 spec 一一对应,位置无关。 */
|
|
18
|
+
export const GLOBAL_OPTION_LINES = [
|
|
19
|
+
' --json 以 JSON 输出完整结果信封,供机器读取。',
|
|
20
|
+
' --dry-run 仅展示计划变更,不写入任何文件(仅作者命令)。',
|
|
21
|
+
' --help 展示帮助;`yoyo-admin --help` / `yoyo-admin create package --help`。',
|
|
22
|
+
' --repo <path> 显式指定仓库查找起点;缺省从当前工作目录向上查找。',
|
|
23
|
+
];
|
|
24
|
+
function formatOptions(options) {
|
|
25
|
+
const width = options.reduce((max, option) => Math.max(max, option.flag.length), 0);
|
|
26
|
+
return options.map((option) => {
|
|
27
|
+
const requirement = option.required ? '必填' : '可选';
|
|
28
|
+
const values = option.values === undefined ? '' : `(${option.values.join(' | ')})`;
|
|
29
|
+
return ` ${option.flag.padEnd(width)} ${requirement} ${option.description}${values}`;
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
export function renderRootHelp(commands) {
|
|
33
|
+
const lines = [
|
|
34
|
+
'yoyo-admin — Yoyo Admin 标准结构统一作者入口',
|
|
35
|
+
'',
|
|
36
|
+
'用法',
|
|
37
|
+
' yoyo-admin <command> [arguments] [options]',
|
|
38
|
+
'',
|
|
39
|
+
'命令',
|
|
40
|
+
];
|
|
41
|
+
for (const command of commands) {
|
|
42
|
+
lines.push(` ${command.name}`);
|
|
43
|
+
lines.push(` ${command.summary}`);
|
|
44
|
+
lines.push(` usage: ${command.usage}`);
|
|
45
|
+
}
|
|
46
|
+
lines.push('', '全局选项(位置无关)', ...GLOBAL_OPTION_LINES);
|
|
47
|
+
lines.push('', '机器可读能力清单', ' yoyo-admin capabilities --json', '', '查看单个命令:yoyo-admin <command> --help');
|
|
48
|
+
return `${lines.join('\n')}\n`;
|
|
49
|
+
}
|
|
50
|
+
export function renderCommandHelp(command) {
|
|
51
|
+
const lines = [
|
|
52
|
+
`yoyo-admin ${command.name} — ${command.summary}`,
|
|
53
|
+
'',
|
|
54
|
+
'用法',
|
|
55
|
+
` ${command.usage}`,
|
|
56
|
+
];
|
|
57
|
+
if (command.arguments.length > 0) {
|
|
58
|
+
lines.push('', '参数');
|
|
59
|
+
for (const argument of command.arguments) {
|
|
60
|
+
lines.push(` ${argument.name}${argument.required ? '(必填)' : ''} ${argument.description}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (command.options.length > 0) {
|
|
64
|
+
lines.push('', '选项', ...formatOptions(command.options.map(option => ({ ...option, required: false }))));
|
|
65
|
+
}
|
|
66
|
+
if (command.booleanFlags?.length) {
|
|
67
|
+
lines.push('', '布尔选项(不接受取值)', ...command.booleanFlags.map(flag => ` ${flag}`));
|
|
68
|
+
}
|
|
69
|
+
lines.push('', '全局选项(位置无关)', ...GLOBAL_OPTION_LINES);
|
|
70
|
+
return `${lines.join('\n')}\n`;
|
|
71
|
+
}
|
|
72
|
+
/** `yoyo-admin create --help`:条目清单 + 各自的 usage。 */
|
|
73
|
+
export function renderRecipeListHelp(items, surface) {
|
|
74
|
+
const lines = [
|
|
75
|
+
`yoyo-admin ${surface.command} — ${surface.title}`,
|
|
76
|
+
'',
|
|
77
|
+
'用法',
|
|
78
|
+
` yoyo-admin ${surface.command} <${items.map(item => item.name).join('|')}> ${surface.usageSuffix}`,
|
|
79
|
+
'',
|
|
80
|
+
surface.itemLabel,
|
|
81
|
+
];
|
|
82
|
+
for (const item of items) {
|
|
83
|
+
lines.push(` ${item.name}`);
|
|
84
|
+
lines.push(` ${item.summary}`);
|
|
85
|
+
lines.push(` usage: ${item.usage}`);
|
|
86
|
+
if (item.kinds.length > 0) {
|
|
87
|
+
lines.push(` kinds: ${item.kinds.join(' | ')}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
lines.push('', surface.itemUsageHint, '', '全局选项(位置无关)', ...GLOBAL_OPTION_LINES);
|
|
91
|
+
return `${lines.join('\n')}\n`;
|
|
92
|
+
}
|
|
93
|
+
/** `yoyo-admin create <recipe> --help`:单条目的完整参数。 */
|
|
94
|
+
export function renderRecipeHelp(item, surface) {
|
|
95
|
+
const lines = [
|
|
96
|
+
`yoyo-admin ${surface.command} ${item.name} — ${item.summary}`,
|
|
97
|
+
'',
|
|
98
|
+
'用法',
|
|
99
|
+
` ${item.usage}`,
|
|
100
|
+
];
|
|
101
|
+
if (item.kinds.length > 0) {
|
|
102
|
+
lines.push('', `kind: ${item.kinds.join(' | ')}`);
|
|
103
|
+
}
|
|
104
|
+
if (item.options.length > 0) {
|
|
105
|
+
lines.push('', '选项', ...formatOptions(item.options));
|
|
106
|
+
}
|
|
107
|
+
lines.push('', '全局选项(位置无关)', ...GLOBAL_OPTION_LINES);
|
|
108
|
+
return `${lines.join('\n')}\n`;
|
|
109
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* workspace lockfile 闭环。
|
|
3
|
+
*
|
|
4
|
+
* **正式语义(冻结)**:
|
|
5
|
+
* ```text
|
|
6
|
+
* create package
|
|
7
|
+
* → 产生包源码
|
|
8
|
+
* → `pnpm install --lockfile-only` 刷新 workspace importer
|
|
9
|
+
* → 成功 == 「仓库可提交状态成立」(源码 + lockfile 可复现)
|
|
10
|
+
* pnpm install --frozen-lockfile
|
|
11
|
+
* → 建立/同步本机 node_modules(本机环境状态,不属于 CLI 的产物)
|
|
12
|
+
* ```
|
|
13
|
+
* 因此本模块**绝不**跑完整 install:不重建依赖树、不写 node_modules。
|
|
14
|
+
*
|
|
15
|
+
* 为什么必须有:`pnpm-workspace.yaml` 的 `'.'` + `'./*'` 会自动把新目录算作
|
|
16
|
+
* workspace 成员,但 `pnpm-lock.yaml` 的 `importers` 不会自己长出来。不刷新的话,
|
|
17
|
+
* 当前环境的 node_modules 里恰好有依赖时 `package:verify` 能过,而 clean checkout
|
|
18
|
+
* 上的 `pnpm install --frozen-lockfile` 会因为「manifest 有、lockfile 无」直接失败。
|
|
19
|
+
*
|
|
20
|
+
* 强 fail-closed:**不依赖 pnpm「通常会做对」**。刷新后必须证明目标 importer
|
|
21
|
+
* 真的出现在 `importers:` 段里 —— 「exit 0 且字节没变」只有在 importer 本来就在
|
|
22
|
+
* 时才合法;字节相同但 importer 缺失一律视为失败并还原 lockfile。
|
|
23
|
+
*/
|
|
24
|
+
export declare const LOCKFILE_RELATIVE = "pnpm-lock.yaml";
|
|
25
|
+
/** pnpm 只读刷新的固定参数:`--lockfile-only` 之外不加任何隐式开关。 */
|
|
26
|
+
export declare const LOCKFILE_REFRESH_ARGS: readonly ["install", "--lockfile-only"];
|
|
27
|
+
export interface PnpmRunResult {
|
|
28
|
+
readonly status: number | null;
|
|
29
|
+
readonly stdout: string;
|
|
30
|
+
readonly stderr: string;
|
|
31
|
+
readonly error?: Error;
|
|
32
|
+
}
|
|
33
|
+
/** 可注入的 pnpm 执行器(默认 spawnSync;测试用它做故障注入)。 */
|
|
34
|
+
export type PnpmRunner = (args: readonly string[], cwd: string) => PnpmRunResult;
|
|
35
|
+
export interface LockfileRefreshOptions {
|
|
36
|
+
/**
|
|
37
|
+
* 必须出现在刷新后 `importers:` 段里的 workspace 目录名(例如 `admin-runtime`)。
|
|
38
|
+
* 必填:新增 package 的闭环不允许「只让 pnpm 跑完就算数」。
|
|
39
|
+
*/
|
|
40
|
+
readonly expectedImporter: string;
|
|
41
|
+
readonly run?: PnpmRunner;
|
|
42
|
+
}
|
|
43
|
+
export interface LockfileRefreshOutcome {
|
|
44
|
+
/** 被刷新的 lockfile 绝对路径。 */
|
|
45
|
+
readonly lockfilePath: string;
|
|
46
|
+
/**
|
|
47
|
+
* lockfile 内容是否发生变化。
|
|
48
|
+
* `false` 的合法含义只有一个:目标 importer **本来就已经存在**(幂等刷新)。
|
|
49
|
+
*/
|
|
50
|
+
readonly changed: boolean;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* 判定 lockfile 的顶层 `importers:` 段里是否存在指定 importer。
|
|
54
|
+
*
|
|
55
|
+
* 手写扫描而非引入 YAML 依赖(本包零 runtime 依赖):
|
|
56
|
+
* - 只在顶层 `importers:` 行之后扫描;
|
|
57
|
+
* - importer key 固定缩进 2 空格(其子键为 4+ 空格,天然不会误匹配);
|
|
58
|
+
* - key 允许 pnpm 的引号形式;
|
|
59
|
+
* - 遇到回到第 0 列的下一段即停止。
|
|
60
|
+
*/
|
|
61
|
+
export declare function lockfileHasImporter(content: string, importer: string): boolean;
|
|
62
|
+
/**
|
|
63
|
+
* 刷新 workspace lockfile 的 importers,并断言目标 importer 确实进入。
|
|
64
|
+
*
|
|
65
|
+
* 失败语义(fail-closed,全部还原 lockfile 字节后抛
|
|
66
|
+
* `YoyoAdminCliUsageError('lockfile-refresh-failed')`):
|
|
67
|
+
* - pnpm 不可执行 / 退出码非 0;
|
|
68
|
+
* - pnpm 报成功但没有 lockfile;
|
|
69
|
+
* - pnpm 报成功但 `importers:` 段里**仍然找不到** `expectedImporter`
|
|
70
|
+
* (含「字节没变」这种最可疑的情形)。
|
|
71
|
+
*/
|
|
72
|
+
export declare function refreshWorkspaceLockfile(adminWorkspace: string, options: LockfileRefreshOptions): LockfileRefreshOutcome;
|
|
73
|
+
//# sourceMappingURL=lockfile.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lockfile.d.ts","sourceRoot":"","sources":["../../src/kernel/lockfile.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAQH,eAAO,MAAM,iBAAiB,mBAAmB,CAAA;AAEjD,mDAAmD;AACnD,eAAO,MAAM,qBAAqB,yCAA0C,CAAA;AAE5E,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,CAAA;CACvB;AAED,6CAA6C;AAC7C,MAAM,MAAM,UAAU,GAAG,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,EAAE,GAAG,EAAE,MAAM,KAAK,aAAa,CAAA;AAEhF,MAAM,WAAW,sBAAsB;IACrC;;;OAGG;IACH,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAA;IACjC,QAAQ,CAAC,GAAG,CAAC,EAAE,UAAU,CAAA;CAC1B;AAED,MAAM,WAAW,sBAAsB;IACrC,0BAA0B;IAC1B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;IAC7B;;;OAGG;IACH,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;CAC1B;AA0BD;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAmB9E;AAED;;;;;;;;;GASG;AACH,wBAAgB,wBAAwB,CACtC,cAAc,EAAE,MAAM,EACtB,OAAO,EAAE,sBAAsB,GAC9B,sBAAsB,CAsCxB"}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* workspace lockfile 闭环。
|
|
3
|
+
*
|
|
4
|
+
* **正式语义(冻结)**:
|
|
5
|
+
* ```text
|
|
6
|
+
* create package
|
|
7
|
+
* → 产生包源码
|
|
8
|
+
* → `pnpm install --lockfile-only` 刷新 workspace importer
|
|
9
|
+
* → 成功 == 「仓库可提交状态成立」(源码 + lockfile 可复现)
|
|
10
|
+
* pnpm install --frozen-lockfile
|
|
11
|
+
* → 建立/同步本机 node_modules(本机环境状态,不属于 CLI 的产物)
|
|
12
|
+
* ```
|
|
13
|
+
* 因此本模块**绝不**跑完整 install:不重建依赖树、不写 node_modules。
|
|
14
|
+
*
|
|
15
|
+
* 为什么必须有:`pnpm-workspace.yaml` 的 `'.'` + `'./*'` 会自动把新目录算作
|
|
16
|
+
* workspace 成员,但 `pnpm-lock.yaml` 的 `importers` 不会自己长出来。不刷新的话,
|
|
17
|
+
* 当前环境的 node_modules 里恰好有依赖时 `package:verify` 能过,而 clean checkout
|
|
18
|
+
* 上的 `pnpm install --frozen-lockfile` 会因为「manifest 有、lockfile 无」直接失败。
|
|
19
|
+
*
|
|
20
|
+
* 强 fail-closed:**不依赖 pnpm「通常会做对」**。刷新后必须证明目标 importer
|
|
21
|
+
* 真的出现在 `importers:` 段里 —— 「exit 0 且字节没变」只有在 importer 本来就在
|
|
22
|
+
* 时才合法;字节相同但 importer 缺失一律视为失败并还原 lockfile。
|
|
23
|
+
*/
|
|
24
|
+
import { existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
25
|
+
import { join } from 'node:path';
|
|
26
|
+
import { YoyoAdminCliUsageError } from './result.js';
|
|
27
|
+
import { runProcess } from './process-runner.js';
|
|
28
|
+
export const LOCKFILE_RELATIVE = 'pnpm-lock.yaml';
|
|
29
|
+
/** pnpm 只读刷新的固定参数:`--lockfile-only` 之外不加任何隐式开关。 */
|
|
30
|
+
export const LOCKFILE_REFRESH_ARGS = ['install', '--lockfile-only'];
|
|
31
|
+
function defaultPnpmRunner(args, cwd) {
|
|
32
|
+
const probe = runProcess({ command: 'pnpm', args, cwd });
|
|
33
|
+
return {
|
|
34
|
+
status: probe.status,
|
|
35
|
+
stdout: String(probe.stdout ?? ''),
|
|
36
|
+
stderr: String(probe.stderr ?? ''),
|
|
37
|
+
...(probe.error === undefined ? {} : { error: probe.error }),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function describeFailure(result) {
|
|
41
|
+
const raw = result.error?.message ?? (result.stderr.trim() || result.stdout.trim());
|
|
42
|
+
return raw.length === 0 ? '(pnpm 未输出任何诊断)' : raw.slice(-800);
|
|
43
|
+
}
|
|
44
|
+
/** 把 lockfile 还原成执行前的字节;原本不存在则删除。 */
|
|
45
|
+
function restoreLockfile(lockfilePath, before) {
|
|
46
|
+
if (before === undefined) {
|
|
47
|
+
if (existsSync(lockfilePath))
|
|
48
|
+
rmSync(lockfilePath, { force: true });
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
writeFileSync(lockfilePath, before, 'utf8');
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* 判定 lockfile 的顶层 `importers:` 段里是否存在指定 importer。
|
|
55
|
+
*
|
|
56
|
+
* 手写扫描而非引入 YAML 依赖(本包零 runtime 依赖):
|
|
57
|
+
* - 只在顶层 `importers:` 行之后扫描;
|
|
58
|
+
* - importer key 固定缩进 2 空格(其子键为 4+ 空格,天然不会误匹配);
|
|
59
|
+
* - key 允许 pnpm 的引号形式;
|
|
60
|
+
* - 遇到回到第 0 列的下一段即停止。
|
|
61
|
+
*/
|
|
62
|
+
export function lockfileHasImporter(content, importer) {
|
|
63
|
+
const lines = content.split(/\r?\n/);
|
|
64
|
+
const start = lines.findIndex(line => /^importers:\s*$/.test(line));
|
|
65
|
+
if (start < 0)
|
|
66
|
+
return false;
|
|
67
|
+
for (let index = start + 1; index < lines.length; index += 1) {
|
|
68
|
+
const line = lines[index] ?? '';
|
|
69
|
+
if (line.trim().length === 0 || line.trimStart().startsWith('#'))
|
|
70
|
+
continue;
|
|
71
|
+
// 回到顶层 key:importers 段结束。
|
|
72
|
+
if (!line.startsWith(' '))
|
|
73
|
+
break;
|
|
74
|
+
const match = /^ {2}(?:'([^']+)'|"([^"]+)"|([^:\s][^:]*?)):\s*$/.exec(line);
|
|
75
|
+
if (match === null)
|
|
76
|
+
continue;
|
|
77
|
+
const key = match[1] ?? match[2] ?? match[3];
|
|
78
|
+
if (key === importer)
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* 刷新 workspace lockfile 的 importers,并断言目标 importer 确实进入。
|
|
85
|
+
*
|
|
86
|
+
* 失败语义(fail-closed,全部还原 lockfile 字节后抛
|
|
87
|
+
* `YoyoAdminCliUsageError('lockfile-refresh-failed')`):
|
|
88
|
+
* - pnpm 不可执行 / 退出码非 0;
|
|
89
|
+
* - pnpm 报成功但没有 lockfile;
|
|
90
|
+
* - pnpm 报成功但 `importers:` 段里**仍然找不到** `expectedImporter`
|
|
91
|
+
* (含「字节没变」这种最可疑的情形)。
|
|
92
|
+
*/
|
|
93
|
+
export function refreshWorkspaceLockfile(adminWorkspace, options) {
|
|
94
|
+
const run = options.run ?? defaultPnpmRunner;
|
|
95
|
+
const lockfilePath = join(adminWorkspace, LOCKFILE_RELATIVE);
|
|
96
|
+
const before = existsSync(lockfilePath) ? readFileSync(lockfilePath, 'utf8') : undefined;
|
|
97
|
+
const result = run(LOCKFILE_REFRESH_ARGS, adminWorkspace);
|
|
98
|
+
const succeeded = result.error === undefined && result.status === 0;
|
|
99
|
+
if (!succeeded) {
|
|
100
|
+
restoreLockfile(lockfilePath, before);
|
|
101
|
+
throw new YoyoAdminCliUsageError('lockfile-refresh-failed', `新增 workspace package 后刷新 ${LOCKFILE_RELATIVE} 失败;本次生成已回滚,工作树保持原状。` +
|
|
102
|
+
`请手工执行 \`pnpm --dir <packages/yoyo-admin> ${LOCKFILE_REFRESH_ARGS.join(' ')}\` 后重试。` +
|
|
103
|
+
`(${describeFailure(result)})`);
|
|
104
|
+
}
|
|
105
|
+
if (!existsSync(lockfilePath)) {
|
|
106
|
+
restoreLockfile(lockfilePath, before);
|
|
107
|
+
throw new YoyoAdminCliUsageError('lockfile-refresh-failed', `pnpm install --lockfile-only 未生成 ${LOCKFILE_RELATIVE};无法保证 workspace 安装闭环。`);
|
|
108
|
+
}
|
|
109
|
+
const after = readFileSync(lockfilePath, 'utf8');
|
|
110
|
+
if (!lockfileHasImporter(after, options.expectedImporter)) {
|
|
111
|
+
restoreLockfile(lockfilePath, before);
|
|
112
|
+
throw new YoyoAdminCliUsageError('lockfile-refresh-failed', `刷新后 ${LOCKFILE_RELATIVE} 的 importers 中仍找不到 ${options.expectedImporter};` +
|
|
113
|
+
'拒绝把「pnpm 报了成功」当作安装闭环成立(本次生成已回滚)。' +
|
|
114
|
+
`请手工执行 \`pnpm --dir <packages/yoyo-admin> ${LOCKFILE_REFRESH_ARGS.join(' ')}\` 确认 importer 已写入。`);
|
|
115
|
+
}
|
|
116
|
+
return { lockfilePath, changed: before !== after };
|
|
117
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `app/manifests.ts` 接线。
|
|
3
|
+
*
|
|
4
|
+
* 这是「统一作者入口」的关键:CLI 必须把 import 与注册一并纳入同一个 FsPlan,
|
|
5
|
+
* 而不是留下「请人工复制这段代码」。
|
|
6
|
+
*
|
|
7
|
+
* 但同时必须 **fail-closed**:
|
|
8
|
+
* - 无法识别当前文件结构 → 零写入直接失败;
|
|
9
|
+
* - 已存在重复 moduleKey / import 标识 → 零写入直接失败。
|
|
10
|
+
*
|
|
11
|
+
* 实现方式是**按行**操作,而不是按字符偏移:行边界天然稳定,
|
|
12
|
+
* 不受插入/删除导致的偏移漂移影响。
|
|
13
|
+
*/
|
|
14
|
+
/** `manifests.ts` 的稳定锚点(全部以行号表达)。 */
|
|
15
|
+
export interface ManifestsFileAnchors {
|
|
16
|
+
/** 最后一个 import 行的行号(0-based,含)。 */
|
|
17
|
+
readonly lastImportLine: number;
|
|
18
|
+
/** `appModuleManifests` 数组起始 `[` 所在行号(0-based)。 */
|
|
19
|
+
readonly arrayOpeningLine: number;
|
|
20
|
+
/** 数组闭合 `]` 所在行号(0-based)。 */
|
|
21
|
+
readonly arrayClosingLine: number;
|
|
22
|
+
/** 当前已注册的 manifest 标识符。 */
|
|
23
|
+
readonly registeredIdentifiers: readonly string[];
|
|
24
|
+
/** 数组中的原始 entry(保留 `...spread`,供单行结构重渲染)。 */
|
|
25
|
+
readonly registeredEntries: readonly string[];
|
|
26
|
+
/** formatter 可能保留的合法单行或多行布局。 */
|
|
27
|
+
readonly arrayLayout: 'inline' | 'multiline';
|
|
28
|
+
/** 单行布局中从行首到 `[`(含)的稳定前缀。 */
|
|
29
|
+
readonly inlinePrefix?: string;
|
|
30
|
+
/** 单行布局中从 `]`(含)到行尾的稳定后缀。 */
|
|
31
|
+
readonly inlineSuffix?: string;
|
|
32
|
+
}
|
|
33
|
+
export interface ManifestsWiringRequest {
|
|
34
|
+
readonly manifestExportName: string;
|
|
35
|
+
readonly importPath: string;
|
|
36
|
+
}
|
|
37
|
+
export declare function parseManifestsAnchors(source: string, collectionName?: string): ManifestsFileAnchors;
|
|
38
|
+
export declare function assertWiringIsSafe(anchors: ManifestsFileAnchors, request: ManifestsWiringRequest, source: string): void;
|
|
39
|
+
export declare function renderWiredManifests(source: string, anchors: ManifestsFileAnchors, request: ManifestsWiringRequest): string;
|
|
40
|
+
export declare function buildManifestsWiring(options: {
|
|
41
|
+
readonly manifestExportName: string;
|
|
42
|
+
readonly importPath: string;
|
|
43
|
+
}): ManifestsWiringRequest;
|
|
44
|
+
//# sourceMappingURL=manifests-wiring.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifests-wiring.d.ts","sourceRoot":"","sources":["../../src/kernel/manifests-wiring.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAIH,qCAAqC;AACrC,MAAM,WAAW,oBAAoB;IACnC,mCAAmC;IACnC,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAA;IAC/B,mDAAmD;IACnD,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAA;IACjC,8BAA8B;IAC9B,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAA;IACjC,2BAA2B;IAC3B,QAAQ,CAAC,qBAAqB,EAAE,SAAS,MAAM,EAAE,CAAA;IACjD,6CAA6C;IAC7C,QAAQ,CAAC,iBAAiB,EAAE,SAAS,MAAM,EAAE,CAAA;IAC7C,gCAAgC;IAChC,QAAQ,CAAC,WAAW,EAAE,QAAQ,GAAG,WAAW,CAAA;IAC5C,6BAA6B;IAC7B,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAA;IAC9B,6BAA6B;IAC7B,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAA;CAC/B;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAA;IACnC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;CAC5B;AAkDD,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,MAAM,EACd,cAAc,SAAuB,GACpC,oBAAoB,CAkGtB;AAED,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,oBAAoB,EAC7B,OAAO,EAAE,sBAAsB,EAC/B,MAAM,EAAE,MAAM,GACb,IAAI,CAqBN;AAED,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,oBAAoB,EAC7B,OAAO,EAAE,sBAAsB,GAC9B,MAAM,CAiDR;AAED,wBAAgB,oBAAoB,CAAC,OAAO,EAAE;IAC5C,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAA;IACnC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;CAC5B,GAAG,sBAAsB,CAKzB"}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `app/manifests.ts` 接线。
|
|
3
|
+
*
|
|
4
|
+
* 这是「统一作者入口」的关键:CLI 必须把 import 与注册一并纳入同一个 FsPlan,
|
|
5
|
+
* 而不是留下「请人工复制这段代码」。
|
|
6
|
+
*
|
|
7
|
+
* 但同时必须 **fail-closed**:
|
|
8
|
+
* - 无法识别当前文件结构 → 零写入直接失败;
|
|
9
|
+
* - 已存在重复 moduleKey / import 标识 → 零写入直接失败。
|
|
10
|
+
*
|
|
11
|
+
* 实现方式是**按行**操作,而不是按字符偏移:行边界天然稳定,
|
|
12
|
+
* 不受插入/删除导致的偏移漂移影响。
|
|
13
|
+
*/
|
|
14
|
+
import { YoyoAdminCliUsageError } from './result.js';
|
|
15
|
+
const IMPORT_LINE_PATTERN = /^import\s+(?:type\s+)?\{[^}]*\}\s+from\s+'[^']*'\s*$/;
|
|
16
|
+
const MANIFESTS_PRINT_WIDTH = 100;
|
|
17
|
+
function escapeRegExp(value) {
|
|
18
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
19
|
+
}
|
|
20
|
+
function parseRegisteredEntries(rawEntries, collectionName, layout) {
|
|
21
|
+
const entries = [];
|
|
22
|
+
const identifiers = [];
|
|
23
|
+
const meaningfulEntries = rawEntries.map((entry) => entry.trim()).filter(Boolean);
|
|
24
|
+
for (const [index, rawEntry] of meaningfulEntries.entries()) {
|
|
25
|
+
if (layout === 'multiline' && index < meaningfulEntries.length - 1 && !rawEntry.endsWith(',')) {
|
|
26
|
+
throw new YoyoAdminCliUsageError('manifests-structure-unrecognized', `${collectionName} 的多行数组项“${rawEntry}”缺少分隔逗号;拒绝写入。`);
|
|
27
|
+
}
|
|
28
|
+
const entry = rawEntry.trim().replace(/,$/, '').trim();
|
|
29
|
+
const match = /^(?:\.\.\.)?([A-Za-z_$][\w$]*)$/.exec(entry);
|
|
30
|
+
if (match === null) {
|
|
31
|
+
throw new YoyoAdminCliUsageError('manifests-structure-unrecognized', `${collectionName} 包含无法识别的数组项“${entry}”;只接受标识符或 spread 标识符,拒绝写入。`);
|
|
32
|
+
}
|
|
33
|
+
const identifier = match[1];
|
|
34
|
+
if (identifiers.includes(identifier)) {
|
|
35
|
+
throw new YoyoAdminCliUsageError('manifests-duplicate-registration', `${collectionName} 中重复注册 ${identifier};拒绝写入。`);
|
|
36
|
+
}
|
|
37
|
+
entries.push(entry);
|
|
38
|
+
identifiers.push(identifier);
|
|
39
|
+
}
|
|
40
|
+
return { entries, identifiers };
|
|
41
|
+
}
|
|
42
|
+
export function parseManifestsAnchors(source, collectionName = 'appModuleManifests') {
|
|
43
|
+
const lines = source.split(/\r?\n/);
|
|
44
|
+
const collectionDeclaration = collectionName === 'appModuleManifests'
|
|
45
|
+
? `${collectionName}\\s*:\\s*YoyoAdminModuleManifest\\[\\]`
|
|
46
|
+
: escapeRegExp(collectionName);
|
|
47
|
+
const arrayOpenLinePattern = new RegExp(`^(export\\s+const\\s+${collectionDeclaration}\\s*=\\s*\\[)\\s*$`);
|
|
48
|
+
const inlineArrayPattern = new RegExp(`^(export\\s+const\\s+${collectionDeclaration}\\s*=\\s*\\[)\\s*(.*?)\\s*(\\](?:\\s+as\\s+const)?\\s*)$`);
|
|
49
|
+
const collectionOwnerPattern = new RegExp(`^export\\s+const\\s+${escapeRegExp(collectionName)}(?:\\s|:)`);
|
|
50
|
+
const ownerLines = lines.flatMap((line, index) => collectionOwnerPattern.test(line) ? [index] : []);
|
|
51
|
+
if (ownerLines.length !== 1) {
|
|
52
|
+
throw new YoyoAdminCliUsageError('manifests-structure-unrecognized', ownerLines.length === 0
|
|
53
|
+
? `无法在 app/manifests.ts 中定位 ${collectionName} 合法单行/多行数组声明;拒绝写入。`
|
|
54
|
+
: `app/manifests.ts 中存在多个 ${collectionName} 数组声明;拒绝写入。`);
|
|
55
|
+
}
|
|
56
|
+
const arrayOpeningLine = ownerLines[0];
|
|
57
|
+
const declarationLine = lines[arrayOpeningLine];
|
|
58
|
+
const inlineMatch = inlineArrayPattern.exec(declarationLine);
|
|
59
|
+
const isMultiline = arrayOpenLinePattern.test(declarationLine);
|
|
60
|
+
if (inlineMatch === null && !isMultiline) {
|
|
61
|
+
throw new YoyoAdminCliUsageError('manifests-structure-unrecognized', `${collectionName} 不是可识别的单行/多行数组声明;拒绝写入。`);
|
|
62
|
+
}
|
|
63
|
+
let arrayClosingLine = arrayOpeningLine;
|
|
64
|
+
let arrayLayout = 'inline';
|
|
65
|
+
let inlinePrefix;
|
|
66
|
+
let inlineSuffix;
|
|
67
|
+
let parsedEntries;
|
|
68
|
+
if (inlineMatch !== null) {
|
|
69
|
+
inlinePrefix = inlineMatch[1];
|
|
70
|
+
inlineSuffix = inlineMatch[3];
|
|
71
|
+
parsedEntries = parseRegisteredEntries((inlineMatch[2] ?? '').split(','), collectionName, 'inline');
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
arrayLayout = 'multiline';
|
|
75
|
+
arrayClosingLine = -1;
|
|
76
|
+
for (let index = arrayOpeningLine + 1; index < lines.length; index += 1) {
|
|
77
|
+
if (/^\](?:\s+as\s+const)?$/.test(lines[index]?.trim() ?? '')) {
|
|
78
|
+
arrayClosingLine = index;
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (arrayClosingLine < 0) {
|
|
83
|
+
throw new YoyoAdminCliUsageError('manifests-structure-unrecognized', `${collectionName} 数组未以行首 \`]\` 结束;拒绝写入。`);
|
|
84
|
+
}
|
|
85
|
+
parsedEntries = parseRegisteredEntries(lines.slice(arrayOpeningLine + 1, arrayClosingLine), collectionName, 'multiline');
|
|
86
|
+
}
|
|
87
|
+
// 最后一个 import 行必须位于数组之前。
|
|
88
|
+
let lastImportLine = -1;
|
|
89
|
+
for (let index = 0; index < arrayOpeningLine; index += 1) {
|
|
90
|
+
if (IMPORT_LINE_PATTERN.test(lines[index] ?? '')) {
|
|
91
|
+
lastImportLine = index;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (lastImportLine < 0) {
|
|
95
|
+
throw new YoyoAdminCliUsageError('manifests-structure-unrecognized', '无法在 app/manifests.ts 中定位 import 区块;拒绝写入。');
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
lastImportLine,
|
|
99
|
+
arrayOpeningLine,
|
|
100
|
+
arrayClosingLine,
|
|
101
|
+
registeredIdentifiers: parsedEntries.identifiers,
|
|
102
|
+
registeredEntries: parsedEntries.entries,
|
|
103
|
+
arrayLayout,
|
|
104
|
+
...(inlinePrefix === undefined ? {} : { inlinePrefix }),
|
|
105
|
+
...(inlineSuffix === undefined ? {} : { inlineSuffix }),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
export function assertWiringIsSafe(anchors, request, source) {
|
|
109
|
+
if (anchors.registeredIdentifiers.includes(request.manifestExportName)) {
|
|
110
|
+
throw new YoyoAdminCliUsageError('manifests-duplicate-registration', `appModuleManifests 中已注册 ${request.manifestExportName};拒绝重复写入。`);
|
|
111
|
+
}
|
|
112
|
+
if (source.includes(`from '${request.importPath}'`)) {
|
|
113
|
+
throw new YoyoAdminCliUsageError('manifests-duplicate-import-path', `app/manifests.ts 中已引用 ${request.importPath};拒绝重复写入。`);
|
|
114
|
+
}
|
|
115
|
+
if (source.includes(request.manifestExportName)) {
|
|
116
|
+
throw new YoyoAdminCliUsageError('manifests-duplicate-identifier', `app/manifests.ts 中已出现标识符 ${request.manifestExportName};拒绝重复写入。`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
export function renderWiredManifests(source, anchors, request) {
|
|
120
|
+
assertWiringIsSafe(anchors, request, source);
|
|
121
|
+
// 保持源文件的换行风格(CRLF 文件插入 LF 行会制造混合换行,触发 lint)。
|
|
122
|
+
const usesCrLf = source.includes('\r\n');
|
|
123
|
+
const lines = source.split(/\r?\n/);
|
|
124
|
+
const importLine = `import { ${request.manifestExportName} } from '${request.importPath}'`;
|
|
125
|
+
const registrationLine = ` ${request.manifestExportName},`;
|
|
126
|
+
const next = [];
|
|
127
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
128
|
+
const line = lines[index] ?? '';
|
|
129
|
+
if (index === anchors.lastImportLine) {
|
|
130
|
+
next.push(line);
|
|
131
|
+
next.push(importLine);
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (index === anchors.arrayOpeningLine && anchors.arrayLayout === 'inline') {
|
|
135
|
+
if (anchors.inlinePrefix === undefined || anchors.inlineSuffix === undefined) {
|
|
136
|
+
throw new YoyoAdminCliUsageError('manifests-structure-unrecognized', '单行 manifests 数组缺少可验证的声明边界;拒绝写入。');
|
|
137
|
+
}
|
|
138
|
+
const entries = [...anchors.registeredEntries, request.manifestExportName];
|
|
139
|
+
const inline = `${anchors.inlinePrefix}${entries.join(', ')}${anchors.inlineSuffix}`;
|
|
140
|
+
if (inline.length <= MANIFESTS_PRINT_WIDTH) {
|
|
141
|
+
next.push(inline);
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
next.push(anchors.inlinePrefix);
|
|
145
|
+
next.push(...entries.map((entry) => ` ${entry},`));
|
|
146
|
+
next.push(anchors.inlineSuffix);
|
|
147
|
+
}
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (index === anchors.arrayClosingLine) {
|
|
151
|
+
// 在数组闭合行之前追加注册项。
|
|
152
|
+
next.push(registrationLine);
|
|
153
|
+
next.push(line);
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
next.push(line);
|
|
157
|
+
}
|
|
158
|
+
return next.join(usesCrLf ? '\r\n' : '\n');
|
|
159
|
+
}
|
|
160
|
+
export function buildManifestsWiring(options) {
|
|
161
|
+
return {
|
|
162
|
+
manifestExportName: options.manifestExportName,
|
|
163
|
+
importPath: options.importPath,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FsPlan 落盘执行器。
|
|
3
|
+
*
|
|
4
|
+
* 三段式:
|
|
5
|
+
* 1. **preflight** —— 只读校验全部 change,任何一条不满足就抛错,**零写入**。
|
|
6
|
+
* 2. **apply** —— 事务化写盘:先快照、逐文件落盘;任一文件失败,
|
|
7
|
+
* 回滚本次已产生的全部文件系统影响(恢复 update 的原内容、删除
|
|
8
|
+
* 已 create 的文件与本 CLI 新建的目录),**工作树恢复原状**。
|
|
9
|
+
* 3. 报告实际写入路径。
|
|
10
|
+
*
|
|
11
|
+
* 「零部分写入」由 preflight(fail-closed)与 apply(transactional)共同保证:
|
|
12
|
+
* 即使在 preflight 与 apply 之间文件系统发生变化(竞态),apply 也不会留下半成品。
|
|
13
|
+
*
|
|
14
|
+
* 路径收束(confinement)是本层的第一道动作:**每条 change 的路径都要先通过
|
|
15
|
+
* `confineRepoPath`,全部通过后才允许任何写入**。preflight 与 apply 各自独立
|
|
16
|
+
* 执行这一层,因此写入边界不依赖 generator 自律。
|
|
17
|
+
*/
|
|
18
|
+
import { type FsChange, type FsPlan } from './fs-plan.js';
|
|
19
|
+
export interface PlanPreflightReport {
|
|
20
|
+
readonly targets: readonly {
|
|
21
|
+
readonly path: string;
|
|
22
|
+
readonly kind: FsChange['kind'];
|
|
23
|
+
readonly origin: string;
|
|
24
|
+
readonly summary?: string;
|
|
25
|
+
}[];
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* 把 plan 相对路径收束成仓库内的绝对路径。
|
|
29
|
+
*
|
|
30
|
+
* 两层防御:静态字符串判定(`assertConfinedRelativePath`)+ 解析后的
|
|
31
|
+
* 真实包含关系校验(`resolve` 结果必须仍在 repoRoot 内)。
|
|
32
|
+
*/
|
|
33
|
+
export declare function confineRepoPath(repoRoot: string, relativePath: string): string;
|
|
34
|
+
/**
|
|
35
|
+
* 只读校验。
|
|
36
|
+
*
|
|
37
|
+
* 规则:
|
|
38
|
+
* - 全部路径先收束(非法路径 → `unsafe-plan-path`,零写入);
|
|
39
|
+
* - `create` 目标必须**不存在**(存在即视为重复执行,禁止静默覆盖);
|
|
40
|
+
* 父目录允许不存在 —— 新模块目录按定义就是新的,由 apply 阶段创建。
|
|
41
|
+
* - `update` 目标必须**存在且可读**(防止路径拼写错误被静默新建掩盖)。
|
|
42
|
+
*/
|
|
43
|
+
export declare function preflightFsPlan(repoRoot: string, plan: FsPlan): PlanPreflightReport;
|
|
44
|
+
/**
|
|
45
|
+
* 事务化落盘。
|
|
46
|
+
*
|
|
47
|
+
* 调用前建议先通过 `preflightFsPlan`;即使未调用,本函数自身也保持原子性——
|
|
48
|
+
* 全部路径先收束(非法路径在**任何写入之前**整体失败),任何一条 change 失败
|
|
49
|
+
* (含 update 目标缺失、路径非法等 I/O 错误)都会触发回滚:已 create 的文件被
|
|
50
|
+
* 删除、本 CLI 新建的目录被移除(仅当仍为空)、已 update 的文件恢复写入前的原内容。
|
|
51
|
+
*/
|
|
52
|
+
export declare function applyFsPlan(repoRoot: string, plan: FsPlan): readonly string[];
|
|
53
|
+
/** 已提交的 plan:允许调用方在「apply 之后的额外步骤」失败时显式撤销。 */
|
|
54
|
+
export interface AppliedFsPlan {
|
|
55
|
+
readonly written: readonly string[];
|
|
56
|
+
/**
|
|
57
|
+
* 逆序撤销本次落盘(删除 create 的文件与本 CLI 新建的目录、恢复 update 的原内容)。
|
|
58
|
+
* 幂等且不抛:撤销自身的失败不掩盖调用方真正关心的错误。
|
|
59
|
+
*/
|
|
60
|
+
rollback(): void;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* 事务化落盘并返回可撤销句柄。
|
|
64
|
+
*
|
|
65
|
+
* 用于 `create package` 这类「写文件 + 额外外部步骤(lockfile 刷新)」的闭环:
|
|
66
|
+
* 额外步骤失败时调用 `rollback()`,保证全有或全无。
|
|
67
|
+
*/
|
|
68
|
+
export declare function applyFsPlanTransaction(repoRoot: string, plan: FsPlan): AppliedFsPlan;
|
|
69
|
+
export declare function readRepoFile(repoRoot: string, path: string): string;
|
|
70
|
+
/**
|
|
71
|
+
* 执行门面。
|
|
72
|
+
*
|
|
73
|
+
* 把「preflight → apply」封装成对象,确保调用方无法跳过校验直接写盘。
|
|
74
|
+
*/
|
|
75
|
+
export declare const planRunner: {
|
|
76
|
+
readonly preflight: typeof preflightFsPlan;
|
|
77
|
+
readonly apply: typeof applyFsPlan;
|
|
78
|
+
/** 返回可撤销句柄的 apply;额外外部步骤失败时用 `rollback()` 保证全有或全无。 */
|
|
79
|
+
readonly applyTransaction: typeof applyFsPlanTransaction;
|
|
80
|
+
};
|
|
81
|
+
//# sourceMappingURL=plan-runner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plan-runner.d.ts","sourceRoot":"","sources":["../../src/kernel/plan-runner.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAKH,OAAO,EAA8B,KAAK,QAAQ,EAAE,KAAK,MAAM,EAAE,MAAM,cAAc,CAAA;AAGrF,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,OAAO,EAAE,SAAS;QACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;QACrB,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAA;QAC/B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;QACvB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAC1B,EAAE,CAAA;CACJ;AAYD;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAe9E;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,mBAAmB,CA+BnF;AAoBD;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAE7E;AAED,8CAA8C;AAC9C,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAA;IACnC;;;OAGG;IACH,QAAQ,IAAI,IAAI,CAAA;CACjB;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,aAAa,CAsCpF;AA6BD,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAMnE;AAED;;;;GAIG;AACH,eAAO,MAAM,UAAU;;;IAGrB,sDAAsD;;CAE9C,CAAA"}
|