skill-flow 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +190 -0
- package/README.md +108 -0
- package/README.zh.md +108 -0
- package/dist/adapters/channel-adapters.d.ts +8 -0
- package/dist/adapters/channel-adapters.js +56 -0
- package/dist/adapters/channel-adapters.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +118 -0
- package/dist/cli.js.map +1 -0
- package/dist/domain/types.d.ts +133 -0
- package/dist/domain/types.js +2 -0
- package/dist/domain/types.js.map +1 -0
- package/dist/services/deployment-applier.d.ts +6 -0
- package/dist/services/deployment-applier.js +54 -0
- package/dist/services/deployment-applier.js.map +1 -0
- package/dist/services/deployment-planner.d.ts +11 -0
- package/dist/services/deployment-planner.js +179 -0
- package/dist/services/deployment-planner.js.map +1 -0
- package/dist/services/doctor-service.d.ts +5 -0
- package/dist/services/doctor-service.js +129 -0
- package/dist/services/doctor-service.js.map +1 -0
- package/dist/services/inventory-service.d.ts +14 -0
- package/dist/services/inventory-service.js +186 -0
- package/dist/services/inventory-service.js.map +1 -0
- package/dist/services/skill-flow.d.ts +60 -0
- package/dist/services/skill-flow.js +260 -0
- package/dist/services/skill-flow.js.map +1 -0
- package/dist/services/source-service.d.ts +35 -0
- package/dist/services/source-service.js +270 -0
- package/dist/services/source-service.js.map +1 -0
- package/dist/services/workflow-service.d.ts +5 -0
- package/dist/services/workflow-service.js +32 -0
- package/dist/services/workflow-service.js.map +1 -0
- package/dist/state/store.d.ts +14 -0
- package/dist/state/store.js +59 -0
- package/dist/state/store.js.map +1 -0
- package/dist/tests/skill-flow.test.d.ts +1 -0
- package/dist/tests/skill-flow.test.js +926 -0
- package/dist/tests/skill-flow.test.js.map +1 -0
- package/dist/tui/config-app.d.ts +47 -0
- package/dist/tui/config-app.js +732 -0
- package/dist/tui/config-app.js.map +1 -0
- package/dist/tui/selection-state.d.ts +8 -0
- package/dist/tui/selection-state.js +32 -0
- package/dist/tui/selection-state.js.map +1 -0
- package/dist/utils/constants.d.ts +19 -0
- package/dist/utils/constants.js +164 -0
- package/dist/utils/constants.js.map +1 -0
- package/dist/utils/format.d.ts +6 -0
- package/dist/utils/format.js +45 -0
- package/dist/utils/format.js.map +1 -0
- package/dist/utils/fs.d.ts +10 -0
- package/dist/utils/fs.js +89 -0
- package/dist/utils/fs.js.map +1 -0
- package/dist/utils/git.d.ts +3 -0
- package/dist/utils/git.js +12 -0
- package/dist/utils/git.js.map +1 -0
- package/dist/utils/result.d.ts +4 -0
- package/dist/utils/result.js +15 -0
- package/dist/utils/result.js.map +1 -0
- package/dist/utils/source-id.d.ts +2 -0
- package/dist/utils/source-id.js +16 -0
- package/dist/utils/source-id.js.map +1 -0
- package/img/img-1.jpg +0 -0
- package/package.json +39 -0
- package/src/adapters/channel-adapters.ts +75 -0
- package/src/cli.tsx +147 -0
- package/src/domain/types.ts +175 -0
- package/src/services/deployment-applier.ts +81 -0
- package/src/services/deployment-planner.ts +259 -0
- package/src/services/doctor-service.ts +156 -0
- package/src/services/inventory-service.ts +251 -0
- package/src/services/skill-flow.ts +381 -0
- package/src/services/source-service.ts +427 -0
- package/src/services/workflow-service.ts +56 -0
- package/src/state/store.ts +68 -0
- package/src/tests/skill-flow.test.ts +1184 -0
- package/src/tui/config-app.tsx +1094 -0
- package/src/tui/selection-state.ts +45 -0
- package/src/utils/constants.ts +201 -0
- package/src/utils/format.ts +59 -0
- package/src/utils/fs.ts +102 -0
- package/src/utils/git.ts +16 -0
- package/src/utils/result.ts +23 -0
- package/src/utils/source-id.ts +19 -0
- package/tsconfig.json +22 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import type {
|
|
3
|
+
ChannelDetection,
|
|
4
|
+
DeploymentStrategy,
|
|
5
|
+
DeploymentTargetName,
|
|
6
|
+
LeafRecord,
|
|
7
|
+
} from "../domain/types.js";
|
|
8
|
+
import {
|
|
9
|
+
TARGET_DEFINITIONS,
|
|
10
|
+
} from "../utils/constants.js";
|
|
11
|
+
import { pathExists } from "../utils/fs.js";
|
|
12
|
+
|
|
13
|
+
export interface ChannelAdapter {
|
|
14
|
+
readonly target: DeploymentTargetName;
|
|
15
|
+
readonly strategy: DeploymentStrategy;
|
|
16
|
+
detect(): Promise<ChannelDetection>;
|
|
17
|
+
resolveTargetPath(rootPath: string, linkName: string): string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
class DefaultChannelAdapter implements ChannelAdapter {
|
|
21
|
+
readonly strategy: DeploymentStrategy;
|
|
22
|
+
|
|
23
|
+
constructor(readonly target: DeploymentTargetName) {
|
|
24
|
+
this.strategy = TARGET_DEFINITIONS[target].strategy;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async detect(): Promise<ChannelDetection> {
|
|
28
|
+
const definition = TARGET_DEFINITIONS[this.target];
|
|
29
|
+
const envVar = definition.envVar;
|
|
30
|
+
const override = process.env[envVar];
|
|
31
|
+
const candidates = override ? [override] : definition.writeRootCandidates;
|
|
32
|
+
|
|
33
|
+
for (const candidate of candidates) {
|
|
34
|
+
const rootPath = path.resolve(candidate);
|
|
35
|
+
if (await pathExists(rootPath)) {
|
|
36
|
+
return {
|
|
37
|
+
target: this.target,
|
|
38
|
+
strategy: this.strategy,
|
|
39
|
+
available: true,
|
|
40
|
+
rootPath,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
target: this.target,
|
|
47
|
+
strategy: this.strategy,
|
|
48
|
+
available: false,
|
|
49
|
+
rootPath: path.resolve(candidates[0] ?? "."),
|
|
50
|
+
reason: `Target directory not found. Set ${envVar} or create the agent directory first.`,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
resolveTargetPath(rootPath: string, linkName: string): string {
|
|
55
|
+
return path.join(rootPath, linkName);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function createChannelAdapters(): ChannelAdapter[] {
|
|
60
|
+
return [
|
|
61
|
+
new DefaultChannelAdapter("claude-code"),
|
|
62
|
+
new DefaultChannelAdapter("codex"),
|
|
63
|
+
new DefaultChannelAdapter("cursor"),
|
|
64
|
+
new DefaultChannelAdapter("github-copilot"),
|
|
65
|
+
new DefaultChannelAdapter("gemini-cli"),
|
|
66
|
+
new DefaultChannelAdapter("opencode"),
|
|
67
|
+
new DefaultChannelAdapter("openclaw"),
|
|
68
|
+
new DefaultChannelAdapter("pi"),
|
|
69
|
+
new DefaultChannelAdapter("windsurf"),
|
|
70
|
+
new DefaultChannelAdapter("roo-code"),
|
|
71
|
+
new DefaultChannelAdapter("cline"),
|
|
72
|
+
new DefaultChannelAdapter("amp"),
|
|
73
|
+
new DefaultChannelAdapter("kiro"),
|
|
74
|
+
];
|
|
75
|
+
}
|
package/src/cli.tsx
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
import { render } from "ink";
|
|
5
|
+
import { SkillFlowApp } from "./services/skill-flow.js";
|
|
6
|
+
import { ConfigApp } from "./tui/config-app.js";
|
|
7
|
+
import { formatActionSummary, formatDoctorIssue, formatWorkflowList } from "./utils/format.js";
|
|
8
|
+
|
|
9
|
+
const program = new Command();
|
|
10
|
+
const app = new SkillFlowApp();
|
|
11
|
+
|
|
12
|
+
program
|
|
13
|
+
.name("skill-flow")
|
|
14
|
+
.description("Workflow-first skill projection manager")
|
|
15
|
+
.version("1.0.0");
|
|
16
|
+
|
|
17
|
+
program
|
|
18
|
+
.command("add")
|
|
19
|
+
.argument("<source>", "Git source locator")
|
|
20
|
+
.action(async (source: string) => {
|
|
21
|
+
const result = await app.addSource(source);
|
|
22
|
+
if (!result.ok) {
|
|
23
|
+
printErrors(result.errors);
|
|
24
|
+
process.exitCode = 1;
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
console.log(
|
|
28
|
+
`Added ${result.data.manifest.id} with ${result.data.leafCount} valid skills.`,
|
|
29
|
+
);
|
|
30
|
+
printWarnings(result.warnings.map((warning) => warning.message));
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
program.command("list").action(async () => {
|
|
34
|
+
const result = await app.listWorkflows();
|
|
35
|
+
if (!result.ok) {
|
|
36
|
+
printErrors(result.errors);
|
|
37
|
+
process.exitCode = 1;
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
console.log(formatWorkflowList(result.data.summaries));
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
program.command("config").action(async () => {
|
|
44
|
+
const result = await app.getConfigData();
|
|
45
|
+
if (!result.ok) {
|
|
46
|
+
printErrors(result.errors);
|
|
47
|
+
process.exitCode = 1;
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const initialDrafts = Object.fromEntries(
|
|
52
|
+
result.data.summaries.map((summary) => {
|
|
53
|
+
const targets = summary.bindings.targets;
|
|
54
|
+
const enabledTargets = Object.entries(targets)
|
|
55
|
+
.filter(([, value]) => value?.enabled)
|
|
56
|
+
.map(([target]) => target) as DraftBinding["enabledTargets"];
|
|
57
|
+
const selectedLeafIds = [...new Set(
|
|
58
|
+
enabledTargets.flatMap((target) => targets[target]?.leafIds ?? []),
|
|
59
|
+
)];
|
|
60
|
+
return [
|
|
61
|
+
summary.source.id,
|
|
62
|
+
{
|
|
63
|
+
enabledTargets,
|
|
64
|
+
selectedLeafIds,
|
|
65
|
+
},
|
|
66
|
+
];
|
|
67
|
+
}),
|
|
68
|
+
);
|
|
69
|
+
const availableTargets = await app.getAvailableTargets();
|
|
70
|
+
|
|
71
|
+
const instance = render(
|
|
72
|
+
<ConfigApp
|
|
73
|
+
app={app}
|
|
74
|
+
availableTargets={availableTargets}
|
|
75
|
+
summaries={result.data.summaries}
|
|
76
|
+
initialDrafts={initialDrafts}
|
|
77
|
+
/>,
|
|
78
|
+
);
|
|
79
|
+
await instance.waitUntilExit();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
program
|
|
83
|
+
.command("update")
|
|
84
|
+
.argument("[sourceId]", "Optional workflow group id")
|
|
85
|
+
.option("--all", "Update all registered workflow groups")
|
|
86
|
+
.action(async (sourceId: string | undefined, options: { all?: boolean }) => {
|
|
87
|
+
const ids = options.all || !sourceId ? undefined : [sourceId];
|
|
88
|
+
const result = await app.updateSources(ids);
|
|
89
|
+
if (!result.ok) {
|
|
90
|
+
printErrors(result.errors);
|
|
91
|
+
process.exitCode = 1;
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
for (const item of result.data.updated) {
|
|
95
|
+
console.log(
|
|
96
|
+
`${item.sourceId} changed:${item.changed} +${item.addedLeafIds.length} -${item.removedLeafIds.length} invalidated:${item.invalidatedLeafIds.length}`,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
printWarnings(result.warnings.map((warning) => warning.message));
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
program.command("doctor").action(async () => {
|
|
103
|
+
const result = await app.doctor();
|
|
104
|
+
if (!result.ok) {
|
|
105
|
+
printErrors(result.errors);
|
|
106
|
+
process.exitCode = 1;
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
console.log(result.data.status);
|
|
110
|
+
if (result.data.issues.length === 0) {
|
|
111
|
+
console.log("No issues detected.");
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
for (const issue of result.data.issues) {
|
|
115
|
+
console.log(formatDoctorIssue(issue));
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
program
|
|
120
|
+
.command("uninstall")
|
|
121
|
+
.argument("<sourceIds...>", "Workflow group ids to remove")
|
|
122
|
+
.action(async (sourceIds: string[]) => {
|
|
123
|
+
const result = await app.uninstall(sourceIds);
|
|
124
|
+
if (!result.ok) {
|
|
125
|
+
printErrors(result.errors);
|
|
126
|
+
process.exitCode = 1;
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
console.log(`Removed: ${result.data.removed.join(", ")}`);
|
|
130
|
+
printWarnings(result.data.warnings);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
await program.parseAsync(process.argv);
|
|
134
|
+
|
|
135
|
+
function printErrors(errors: Array<{ message: string }>) {
|
|
136
|
+
for (const error of errors) {
|
|
137
|
+
console.error(error.message);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function printWarnings(messages: string[]) {
|
|
142
|
+
for (const message of messages) {
|
|
143
|
+
console.warn(`warning: ${message}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
type DraftBinding = import("./services/skill-flow.js").DraftBinding;
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
export type Warning = {
|
|
2
|
+
code: string;
|
|
3
|
+
message: string;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
export type Failure = {
|
|
7
|
+
code: string;
|
|
8
|
+
message: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type Result<T> =
|
|
12
|
+
| { ok: true; data: T; warnings: Warning[]; errors: [] }
|
|
13
|
+
| { ok: false; data?: T; warnings: Warning[]; errors: Failure[] };
|
|
14
|
+
|
|
15
|
+
export type SourceKind = "git";
|
|
16
|
+
|
|
17
|
+
export type DeploymentTargetName =
|
|
18
|
+
| "claude-code"
|
|
19
|
+
| "codex"
|
|
20
|
+
| "cursor"
|
|
21
|
+
| "github-copilot"
|
|
22
|
+
| "gemini-cli"
|
|
23
|
+
| "opencode"
|
|
24
|
+
| "openclaw"
|
|
25
|
+
| "pi"
|
|
26
|
+
| "windsurf"
|
|
27
|
+
| "roo-code"
|
|
28
|
+
| "cline"
|
|
29
|
+
| "amp"
|
|
30
|
+
| "kiro";
|
|
31
|
+
|
|
32
|
+
export type DeploymentStrategy = "symlink" | "copy";
|
|
33
|
+
|
|
34
|
+
export type HealthStatus =
|
|
35
|
+
| "HEALTHY"
|
|
36
|
+
| "ACTIVE"
|
|
37
|
+
| "INACTIVE"
|
|
38
|
+
| "PARTIAL"
|
|
39
|
+
| "BLOCKED"
|
|
40
|
+
| "INVALID"
|
|
41
|
+
| "UPDATE AVAILABLE"
|
|
42
|
+
| "UP TO DATE"
|
|
43
|
+
| "DRIFT DETECTED";
|
|
44
|
+
|
|
45
|
+
export type SourceManifestRecord = {
|
|
46
|
+
id: string;
|
|
47
|
+
locator: string;
|
|
48
|
+
kind: SourceKind;
|
|
49
|
+
displayName: string;
|
|
50
|
+
addedAt: string;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export type TargetBinding = {
|
|
54
|
+
enabled: boolean;
|
|
55
|
+
leafIds: string[];
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export type SourceBinding = {
|
|
59
|
+
targets: Partial<Record<DeploymentTargetName, TargetBinding>>;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export type Manifest = {
|
|
63
|
+
schemaVersion: 1;
|
|
64
|
+
sources: SourceManifestRecord[];
|
|
65
|
+
bindings: Record<string, SourceBinding>;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export type InvalidLeafRecord = {
|
|
69
|
+
path: string;
|
|
70
|
+
reason: string;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export type SourceLockRecord = {
|
|
74
|
+
id: string;
|
|
75
|
+
locator: string;
|
|
76
|
+
kind: SourceKind;
|
|
77
|
+
displayName: string;
|
|
78
|
+
checkoutPath: string;
|
|
79
|
+
commitSha: string;
|
|
80
|
+
updatedAt: string;
|
|
81
|
+
leafIds: string[];
|
|
82
|
+
invalidLeafs: InvalidLeafRecord[];
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export type LeafRecord = {
|
|
86
|
+
id: string;
|
|
87
|
+
sourceId: string;
|
|
88
|
+
name: string;
|
|
89
|
+
linkName: string;
|
|
90
|
+
title: string;
|
|
91
|
+
description: string;
|
|
92
|
+
relativePath: string;
|
|
93
|
+
absolutePath: string;
|
|
94
|
+
skillFilePath: string;
|
|
95
|
+
contentHash: string;
|
|
96
|
+
metadataWarnings: string[];
|
|
97
|
+
valid: true;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export type DeploymentRecord = {
|
|
101
|
+
sourceId: string;
|
|
102
|
+
leafId: string;
|
|
103
|
+
target: DeploymentTargetName;
|
|
104
|
+
targetPath: string;
|
|
105
|
+
strategy: DeploymentStrategy;
|
|
106
|
+
status: "active" | "drifted" | "blocked" | "removed";
|
|
107
|
+
contentHash: string;
|
|
108
|
+
appliedAt: string;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
export type LockFile = {
|
|
112
|
+
schemaVersion: 1;
|
|
113
|
+
sources: SourceLockRecord[];
|
|
114
|
+
leafInventory: LeafRecord[];
|
|
115
|
+
deployments: DeploymentRecord[];
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
export type ChannelDetection = {
|
|
119
|
+
target: DeploymentTargetName;
|
|
120
|
+
strategy: DeploymentStrategy;
|
|
121
|
+
available: boolean;
|
|
122
|
+
rootPath: string;
|
|
123
|
+
reason?: string;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
export type DeploymentActionKind =
|
|
127
|
+
| "create"
|
|
128
|
+
| "update"
|
|
129
|
+
| "remove"
|
|
130
|
+
| "noop"
|
|
131
|
+
| "blocked";
|
|
132
|
+
|
|
133
|
+
export type DeploymentAction = {
|
|
134
|
+
kind: DeploymentActionKind;
|
|
135
|
+
sourceId: string;
|
|
136
|
+
leafId: string;
|
|
137
|
+
target: DeploymentTargetName;
|
|
138
|
+
strategy: DeploymentStrategy;
|
|
139
|
+
sourcePath: string;
|
|
140
|
+
targetPath: string;
|
|
141
|
+
previousTargetPath?: string;
|
|
142
|
+
reason?: string;
|
|
143
|
+
contentHash: string;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
export type DeploymentPlan = {
|
|
147
|
+
actions: DeploymentAction[];
|
|
148
|
+
warnings: Warning[];
|
|
149
|
+
blocked: DeploymentAction[];
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
export type DoctorIssueSeverity = "info" | "warning" | "error";
|
|
153
|
+
|
|
154
|
+
export type DoctorIssue = {
|
|
155
|
+
severity: DoctorIssueSeverity;
|
|
156
|
+
sourceId: string;
|
|
157
|
+
target?: DeploymentTargetName;
|
|
158
|
+
leafId?: string;
|
|
159
|
+
code: string;
|
|
160
|
+
message: string;
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
export type DoctorReport = {
|
|
164
|
+
status: "HEALTHY" | "PARTIAL" | "BLOCKED";
|
|
165
|
+
issues: DoctorIssue[];
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
export type WorkflowSummary = {
|
|
169
|
+
source: SourceManifestRecord;
|
|
170
|
+
lock: SourceLockRecord | undefined;
|
|
171
|
+
leafs: LeafRecord[];
|
|
172
|
+
bindings: SourceBinding;
|
|
173
|
+
activeTargetCount: number;
|
|
174
|
+
health: HealthStatus;
|
|
175
|
+
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import type {
|
|
3
|
+
DeploymentAction,
|
|
4
|
+
DeploymentRecord,
|
|
5
|
+
LockFile,
|
|
6
|
+
Result,
|
|
7
|
+
} from "../domain/types.js";
|
|
8
|
+
import { copyDirectory, createSymlink, ensureDir, pathExists, removePath } from "../utils/fs.js";
|
|
9
|
+
import { ok } from "../utils/result.js";
|
|
10
|
+
|
|
11
|
+
export class DeploymentApplier {
|
|
12
|
+
async applyPlan(
|
|
13
|
+
lockFile: LockFile,
|
|
14
|
+
actions: DeploymentAction[],
|
|
15
|
+
): Promise<Result<{ applied: DeploymentAction[] }>> {
|
|
16
|
+
const applied: DeploymentAction[] = [];
|
|
17
|
+
|
|
18
|
+
for (const action of actions) {
|
|
19
|
+
if (action.kind === "blocked" || action.kind === "noop") {
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (action.kind === "remove") {
|
|
24
|
+
if (await pathExists(action.targetPath)) {
|
|
25
|
+
await removePath(action.targetPath);
|
|
26
|
+
}
|
|
27
|
+
lockFile.deployments = lockFile.deployments.filter(
|
|
28
|
+
(deployment) =>
|
|
29
|
+
!(
|
|
30
|
+
deployment.sourceId === action.sourceId &&
|
|
31
|
+
deployment.leafId === action.leafId &&
|
|
32
|
+
deployment.target === action.target
|
|
33
|
+
),
|
|
34
|
+
);
|
|
35
|
+
applied.push(action);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
await ensureDir(path.dirname(action.targetPath));
|
|
40
|
+
if (
|
|
41
|
+
action.previousTargetPath &&
|
|
42
|
+
action.previousTargetPath !== action.targetPath &&
|
|
43
|
+
(await pathExists(action.previousTargetPath))
|
|
44
|
+
) {
|
|
45
|
+
await removePath(action.previousTargetPath);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (action.strategy === "symlink") {
|
|
49
|
+
await createSymlink(action.sourcePath, action.targetPath);
|
|
50
|
+
} else {
|
|
51
|
+
await copyDirectory(action.sourcePath, action.targetPath);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const nextRecord: DeploymentRecord = {
|
|
55
|
+
sourceId: action.sourceId,
|
|
56
|
+
leafId: action.leafId,
|
|
57
|
+
target: action.target,
|
|
58
|
+
targetPath: action.targetPath,
|
|
59
|
+
strategy: action.strategy,
|
|
60
|
+
status: "active",
|
|
61
|
+
contentHash: action.contentHash,
|
|
62
|
+
appliedAt: new Date().toISOString(),
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
lockFile.deployments = [
|
|
66
|
+
...lockFile.deployments.filter(
|
|
67
|
+
(deployment) =>
|
|
68
|
+
!(
|
|
69
|
+
deployment.sourceId === action.sourceId &&
|
|
70
|
+
deployment.leafId === action.leafId &&
|
|
71
|
+
deployment.target === action.target
|
|
72
|
+
),
|
|
73
|
+
),
|
|
74
|
+
nextRecord,
|
|
75
|
+
];
|
|
76
|
+
applied.push(action);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return ok({ applied });
|
|
80
|
+
}
|
|
81
|
+
}
|