sfora-cli 0.15.0 → 0.16.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/README.md +62 -1
- package/dist/local-core/files.d.ts +1 -1
- package/dist/local-core/files.js +2 -2
- package/dist/local-core/index.d.ts +12 -0
- package/dist/local-core/index.js +11 -0
- package/dist/local-core/skill-adapters.d.ts +21 -0
- package/dist/local-core/skill-adapters.js +19 -0
- package/dist/local-core/skill-discovery.d.ts +22 -0
- package/dist/local-core/skill-discovery.js +79 -0
- package/dist/local-core/skill-domain.d.ts +74 -0
- package/dist/local-core/skill-domain.js +1 -0
- package/dist/local-core/skill-executor.d.ts +23 -0
- package/dist/local-core/skill-executor.js +51 -0
- package/dist/local-core/skill-index.d.ts +54 -0
- package/dist/local-core/skill-index.js +115 -0
- package/dist/local-core/skill-local-executor.d.ts +18 -0
- package/dist/local-core/skill-local-executor.js +249 -0
- package/dist/local-core/skill-operations.d.ts +61 -0
- package/dist/local-core/skill-operations.js +268 -0
- package/dist/local-core/skill-review.d.ts +46 -0
- package/dist/local-core/skill-review.js +132 -0
- package/dist/local-core/skill-service.d.ts +96 -0
- package/dist/local-core/skill-service.js +157 -0
- package/dist/local-core/skill-store.d.ts +34 -0
- package/dist/local-core/skill-store.js +187 -0
- package/dist/local-core/skill-sync.d.ts +132 -0
- package/dist/local-core/skill-sync.js +111 -0
- package/dist/local-core/skills.d.ts +10 -0
- package/dist/local-core/skills.js +49 -37
- package/dist/skills-client.d.ts +13 -2
- package/dist/skills-client.js +57 -5
- package/dist/skills-command.d.ts +1 -1
- package/dist/skills-command.js +152 -4
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
package/dist/skills-command.js
CHANGED
|
@@ -1,9 +1,33 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { dirname, join } from "node:path";
|
|
2
|
+
import { readFile, realpath } from "node:fs/promises";
|
|
3
|
+
import { diffSkillBundles, installSkillBundle, inspectSkillTarget, readSkillBundle, discoverSkills, SkillService, uninstallSkill, validateSkillBundle } from "./local-core/index.js";
|
|
4
|
+
import { SkillOperationJournal } from "./local-core/skill-operations.js";
|
|
5
|
+
import { SkillOperationExecutor } from "./local-core/skill-executor.js";
|
|
6
|
+
import { defaultSkillCatalogPath } from "./local-core/skill-store.js";
|
|
3
7
|
import { SkillsClient } from "./skills-client.js";
|
|
8
|
+
import { SkillBindingService, assertSkillBindingSnapshot, compareSkillStates, planSkillTransfer, planSkillInstallation } from './local-core/skill-sync.js';
|
|
4
9
|
export const SKILLS_HELP = `Skills:
|
|
10
|
+
sfora skills inventory --json Persistent local inventory with warnings and identities
|
|
11
|
+
sfora skills roots [add <folder> [label] | add-project <folder> [label] | remove <id>]
|
|
12
|
+
Manage registered discovery folders (never deletes files)
|
|
5
13
|
sfora skills scan [root ...] --json Discover complete local skill bundles
|
|
6
14
|
sfora skills list --project <slug> List cloud project skills
|
|
15
|
+
sfora skills index --json Read cached local inventory without scanning
|
|
16
|
+
sfora skills operations List durable transfer outcomes and recovery needs
|
|
17
|
+
sfora skills queue <plan.json> Queue a reviewed push, pull or install
|
|
18
|
+
sfora skills cancel <operation-id> Cancel an unstarted operation
|
|
19
|
+
sfora skills apply <plan.json|operation-id> Execute a reviewed plan
|
|
20
|
+
sfora skills recover <operation-id> Reconcile an interrupted operation
|
|
21
|
+
sfora skills apply-batch <ids.json> Run queued operation IDs with per-item outcomes
|
|
22
|
+
sfora skills recover-batch <ids.json> Resume incomplete items without replaying successes
|
|
23
|
+
sfora skills bindings List explicit cloud mappings and baselines
|
|
24
|
+
sfora skills bind <location-id> <cloud-name> --project <slug>
|
|
25
|
+
Explicitly adopt a published cloud identity
|
|
26
|
+
sfora skills status <binding-id> Compare current files with the bound published revision
|
|
27
|
+
sfora skills plan <binding-id> <push|pull> Preview IDs, hashes, conflicts and per-file changes
|
|
28
|
+
sfora skills plan-install <name> --project <slug> --skills-target <existing-directory>
|
|
29
|
+
Preview a new installation without modifying files
|
|
30
|
+
sfora skills relocate <location-id> <folder> Preserve identity after an explicit folder move
|
|
7
31
|
sfora skills push <folder> --project <slug> --expected-version <N> [--expected-revision <N>]
|
|
8
32
|
Upload draft and publish (0 for a new skill)
|
|
9
33
|
sfora skills pull <name> <bundle.json> --project <slug> [--version <N>]
|
|
@@ -21,8 +45,35 @@ export async function runSkillsCommand(args, settings) {
|
|
|
21
45
|
const print = (value) => console.log(JSON.stringify(value, null, args.json ? undefined : 2));
|
|
22
46
|
const need = (value, label) => { if (!value)
|
|
23
47
|
throw new Error(`${label} is required.\n${SKILLS_HELP}`); return value; };
|
|
24
|
-
if (verb === "
|
|
25
|
-
|
|
48
|
+
if (verb === "roots") {
|
|
49
|
+
const service = new SkillService();
|
|
50
|
+
if (!first)
|
|
51
|
+
print(await service.roots());
|
|
52
|
+
else if (first === "add" || first === "add-project")
|
|
53
|
+
print(await service.registerRoot(need(second, "Folder"), args.rest[3], first === "add-project" ? "project" : "custom"));
|
|
54
|
+
else if (first === "remove") {
|
|
55
|
+
await service.removeRoot(need(second, "Root ID"));
|
|
56
|
+
print({ removed: second });
|
|
57
|
+
}
|
|
58
|
+
else
|
|
59
|
+
throw new Error("Use skills roots, skills roots add <folder> [label], or skills roots remove <id>.");
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (verb === "index") {
|
|
63
|
+
print(await new SkillService().cachedInventory());
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (verb === "inventory" || verb === "scan") {
|
|
67
|
+
const result = verb === "scan" && args.rest.length > 1
|
|
68
|
+
? await discoverSkills(args.rest.slice(1).map(path => ({ path, label: "Chosen folder", depth: 4 })))
|
|
69
|
+
: await new SkillService().inventory();
|
|
70
|
+
if (verb === "scan") {
|
|
71
|
+
print(result.skills);
|
|
72
|
+
if (result.warnings.length)
|
|
73
|
+
console.error(JSON.stringify({ warnings: result.warnings }));
|
|
74
|
+
}
|
|
75
|
+
else
|
|
76
|
+
print(result);
|
|
26
77
|
return;
|
|
27
78
|
}
|
|
28
79
|
if (verb === "uninstall") {
|
|
@@ -30,6 +81,97 @@ export async function runSkillsCommand(args, settings) {
|
|
|
30
81
|
print({ uninstalled: first });
|
|
31
82
|
return;
|
|
32
83
|
}
|
|
84
|
+
if (['operations', 'queue', 'cancel', 'apply', 'recover', 'apply-batch', 'recover-batch'].includes(verb)) {
|
|
85
|
+
const journal = new SkillOperationJournal(join(dirname(defaultSkillCatalogPath()), 'operations.sqlite'));
|
|
86
|
+
if (verb === 'operations') {
|
|
87
|
+
print(await journal.list());
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (verb === 'cancel') {
|
|
91
|
+
print(await journal.cancel(need(first, 'Operation ID')));
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (verb === 'queue') {
|
|
95
|
+
print(await journal.create(JSON.parse(await readFile(need(first, 'Plan JSON'), 'utf8'))));
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const service = new SkillService();
|
|
99
|
+
const client = new SkillsClient(settings);
|
|
100
|
+
const executor = new SkillOperationExecutor(service.store, journal, client);
|
|
101
|
+
if (verb === 'apply-batch' || verb === 'recover-batch') {
|
|
102
|
+
const ids = JSON.parse(await readFile(need(first, 'Operation IDs JSON'), 'utf8'));
|
|
103
|
+
const controller = new AbortController();
|
|
104
|
+
const stop = () => { controller.abort(); console.error('Stopping after the current item; unstarted work will be cancelled.'); };
|
|
105
|
+
process.once('SIGINT', stop);
|
|
106
|
+
try {
|
|
107
|
+
const results = await executor.batch(ids, { recover: verb === 'recover-batch', signal: controller.signal });
|
|
108
|
+
print(results);
|
|
109
|
+
if (results.some(r => r.error))
|
|
110
|
+
process.exitCode = 1;
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
process.removeListener('SIGINT', stop);
|
|
114
|
+
}
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const operation = verb === 'apply' && !/^[a-f0-9-]{36}$/.test(first ?? '') ? await journal.create(JSON.parse(await readFile(need(first, 'Plan JSON'), 'utf8'))) : undefined;
|
|
118
|
+
const id = operation?.id ?? need(first, 'Operation ID');
|
|
119
|
+
try {
|
|
120
|
+
print(await executor.apply(id, verb === 'recover'));
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
throw new Error(`${error.message} Operation ${id} was retained; inspect skills operations, then use skills recover ${id}.`);
|
|
124
|
+
}
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (['bindings', 'bind', 'status', 'plan', 'relocate'].includes(verb)) {
|
|
128
|
+
const service = new SkillService();
|
|
129
|
+
const bindings = new SkillBindingService(service.store);
|
|
130
|
+
if (verb === 'bindings') {
|
|
131
|
+
print(await bindings.list());
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (verb === 'relocate') {
|
|
135
|
+
print(await service.relocateLocation(need(first, 'Location ID'), need(second, 'Folder')));
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const catalog = await service.store.read();
|
|
139
|
+
const binding = verb === 'bind' ? undefined : catalog.bindings.find(b => b.id === need(first, 'Binding ID'));
|
|
140
|
+
if (verb !== 'bind' && !binding)
|
|
141
|
+
throw new Error('Skill binding not found.');
|
|
142
|
+
if (binding && args.project && args.project !== binding.locator.project)
|
|
143
|
+
throw new Error('--project differs from the explicit skill binding.');
|
|
144
|
+
const location = catalog.locations.find(l => verb === 'bind' ? l.id === first : l.skillId === binding.localSkillId);
|
|
145
|
+
if (!location)
|
|
146
|
+
throw new Error('Local skill location not found. Register its folder and refresh the inventory first.');
|
|
147
|
+
if (verb === 'plan' && second !== 'push' && second !== 'pull')
|
|
148
|
+
throw new Error('Choose skills plan <binding-id> <push|pull>.');
|
|
149
|
+
const client = new SkillsClient(settings);
|
|
150
|
+
const snapshot = await client.snapshot(binding?.locator.project ?? need(args.project, '--project'), binding?.locator.name ?? need(second, 'Cloud skill name'));
|
|
151
|
+
if (binding)
|
|
152
|
+
assertSkillBindingSnapshot(binding, snapshot);
|
|
153
|
+
let local;
|
|
154
|
+
try {
|
|
155
|
+
local = { state: 'available', bundle: await readSkillBundle(location.path) };
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
local = { state: error.code === 'ENOENT' ? 'missing' : 'unavailable', reason: error.message };
|
|
159
|
+
}
|
|
160
|
+
if (verb === 'bind') {
|
|
161
|
+
if (local.state !== 'available')
|
|
162
|
+
throw new Error(local.reason);
|
|
163
|
+
print(await bindings.adopt(location.skillId, snapshot, local.bundle));
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (verb === 'status') {
|
|
167
|
+
print({ bindingId: binding.id, status: compareSkillStates(local, { state: 'available', bundle: snapshot.bundle }, binding.baseline),
|
|
168
|
+
local: local.state === 'available' ? { state: local.state, hash: local.bundle.hash } : local, remote: snapshot.revision, baseline: binding.baseline ?? null });
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const target = await inspectSkillTarget(location.path);
|
|
172
|
+
print(planSkillTransfer({ direction: second, binding: binding, location, local, remote: snapshot, ownership: target.ownership }));
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
33
175
|
if (verb === "install-file") {
|
|
34
176
|
const bundle = JSON.parse(await readFile(need(first, "Bundle JSON"), "utf8"));
|
|
35
177
|
validateSkillBundle(bundle);
|
|
@@ -38,6 +180,12 @@ export async function runSkillsCommand(args, settings) {
|
|
|
38
180
|
}
|
|
39
181
|
const project = need(args.project, "--project");
|
|
40
182
|
const client = new SkillsClient(settings);
|
|
183
|
+
if (verb === 'plan-install') {
|
|
184
|
+
const remote = await client.snapshot(project, need(first, 'Skill name'));
|
|
185
|
+
const target = join(await realpath(need(args.skillsTarget, '--skills-target')), remote.bundle.name);
|
|
186
|
+
print(planSkillInstallation(remote, target, await inspectSkillTarget(target)));
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
41
189
|
if (verb === "list") {
|
|
42
190
|
print(await client.list(project));
|
|
43
191
|
return;
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const CLI_VERSION = "0.
|
|
1
|
+
export declare const CLI_VERSION = "0.16.0";
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Generated by scripts/sync-format.mjs from package.json.
|
|
2
|
-
export const CLI_VERSION = "0.
|
|
2
|
+
export const CLI_VERSION = "0.16.0";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sfora-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Your sfora workspace as a markdown filesystem \u2014 a CLI + MCP server. Post/task/doc, ls/cat/grep, and a shell so agents operate sfora natively.",
|
|
6
6
|
"keywords": [
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"typescript": "^5.9.3"
|
|
52
52
|
},
|
|
53
53
|
"engines": {
|
|
54
|
-
"node": ">=
|
|
54
|
+
"node": ">=22.13.0"
|
|
55
55
|
},
|
|
56
56
|
"exports": {
|
|
57
57
|
".": {
|