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.
Files changed (87) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +108 -0
  3. package/README.zh.md +108 -0
  4. package/dist/adapters/channel-adapters.d.ts +8 -0
  5. package/dist/adapters/channel-adapters.js +56 -0
  6. package/dist/adapters/channel-adapters.js.map +1 -0
  7. package/dist/cli.d.ts +2 -0
  8. package/dist/cli.js +118 -0
  9. package/dist/cli.js.map +1 -0
  10. package/dist/domain/types.d.ts +133 -0
  11. package/dist/domain/types.js +2 -0
  12. package/dist/domain/types.js.map +1 -0
  13. package/dist/services/deployment-applier.d.ts +6 -0
  14. package/dist/services/deployment-applier.js +54 -0
  15. package/dist/services/deployment-applier.js.map +1 -0
  16. package/dist/services/deployment-planner.d.ts +11 -0
  17. package/dist/services/deployment-planner.js +179 -0
  18. package/dist/services/deployment-planner.js.map +1 -0
  19. package/dist/services/doctor-service.d.ts +5 -0
  20. package/dist/services/doctor-service.js +129 -0
  21. package/dist/services/doctor-service.js.map +1 -0
  22. package/dist/services/inventory-service.d.ts +14 -0
  23. package/dist/services/inventory-service.js +186 -0
  24. package/dist/services/inventory-service.js.map +1 -0
  25. package/dist/services/skill-flow.d.ts +60 -0
  26. package/dist/services/skill-flow.js +260 -0
  27. package/dist/services/skill-flow.js.map +1 -0
  28. package/dist/services/source-service.d.ts +35 -0
  29. package/dist/services/source-service.js +270 -0
  30. package/dist/services/source-service.js.map +1 -0
  31. package/dist/services/workflow-service.d.ts +5 -0
  32. package/dist/services/workflow-service.js +32 -0
  33. package/dist/services/workflow-service.js.map +1 -0
  34. package/dist/state/store.d.ts +14 -0
  35. package/dist/state/store.js +59 -0
  36. package/dist/state/store.js.map +1 -0
  37. package/dist/tests/skill-flow.test.d.ts +1 -0
  38. package/dist/tests/skill-flow.test.js +926 -0
  39. package/dist/tests/skill-flow.test.js.map +1 -0
  40. package/dist/tui/config-app.d.ts +47 -0
  41. package/dist/tui/config-app.js +732 -0
  42. package/dist/tui/config-app.js.map +1 -0
  43. package/dist/tui/selection-state.d.ts +8 -0
  44. package/dist/tui/selection-state.js +32 -0
  45. package/dist/tui/selection-state.js.map +1 -0
  46. package/dist/utils/constants.d.ts +19 -0
  47. package/dist/utils/constants.js +164 -0
  48. package/dist/utils/constants.js.map +1 -0
  49. package/dist/utils/format.d.ts +6 -0
  50. package/dist/utils/format.js +45 -0
  51. package/dist/utils/format.js.map +1 -0
  52. package/dist/utils/fs.d.ts +10 -0
  53. package/dist/utils/fs.js +89 -0
  54. package/dist/utils/fs.js.map +1 -0
  55. package/dist/utils/git.d.ts +3 -0
  56. package/dist/utils/git.js +12 -0
  57. package/dist/utils/git.js.map +1 -0
  58. package/dist/utils/result.d.ts +4 -0
  59. package/dist/utils/result.js +15 -0
  60. package/dist/utils/result.js.map +1 -0
  61. package/dist/utils/source-id.d.ts +2 -0
  62. package/dist/utils/source-id.js +16 -0
  63. package/dist/utils/source-id.js.map +1 -0
  64. package/img/img-1.jpg +0 -0
  65. package/package.json +39 -0
  66. package/src/adapters/channel-adapters.ts +75 -0
  67. package/src/cli.tsx +147 -0
  68. package/src/domain/types.ts +175 -0
  69. package/src/services/deployment-applier.ts +81 -0
  70. package/src/services/deployment-planner.ts +259 -0
  71. package/src/services/doctor-service.ts +156 -0
  72. package/src/services/inventory-service.ts +251 -0
  73. package/src/services/skill-flow.ts +381 -0
  74. package/src/services/source-service.ts +427 -0
  75. package/src/services/workflow-service.ts +56 -0
  76. package/src/state/store.ts +68 -0
  77. package/src/tests/skill-flow.test.ts +1184 -0
  78. package/src/tui/config-app.tsx +1094 -0
  79. package/src/tui/selection-state.ts +45 -0
  80. package/src/utils/constants.ts +201 -0
  81. package/src/utils/format.ts +59 -0
  82. package/src/utils/fs.ts +102 -0
  83. package/src/utils/git.ts +16 -0
  84. package/src/utils/result.ts +23 -0
  85. package/src/utils/source-id.ts +19 -0
  86. package/tsconfig.json +22 -0
  87. 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
+ }