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.
Files changed (36) hide show
  1. package/README.md +62 -1
  2. package/dist/local-core/files.d.ts +1 -1
  3. package/dist/local-core/files.js +2 -2
  4. package/dist/local-core/index.d.ts +12 -0
  5. package/dist/local-core/index.js +11 -0
  6. package/dist/local-core/skill-adapters.d.ts +21 -0
  7. package/dist/local-core/skill-adapters.js +19 -0
  8. package/dist/local-core/skill-discovery.d.ts +22 -0
  9. package/dist/local-core/skill-discovery.js +79 -0
  10. package/dist/local-core/skill-domain.d.ts +74 -0
  11. package/dist/local-core/skill-domain.js +1 -0
  12. package/dist/local-core/skill-executor.d.ts +23 -0
  13. package/dist/local-core/skill-executor.js +51 -0
  14. package/dist/local-core/skill-index.d.ts +54 -0
  15. package/dist/local-core/skill-index.js +115 -0
  16. package/dist/local-core/skill-local-executor.d.ts +18 -0
  17. package/dist/local-core/skill-local-executor.js +249 -0
  18. package/dist/local-core/skill-operations.d.ts +61 -0
  19. package/dist/local-core/skill-operations.js +268 -0
  20. package/dist/local-core/skill-review.d.ts +46 -0
  21. package/dist/local-core/skill-review.js +132 -0
  22. package/dist/local-core/skill-service.d.ts +96 -0
  23. package/dist/local-core/skill-service.js +157 -0
  24. package/dist/local-core/skill-store.d.ts +34 -0
  25. package/dist/local-core/skill-store.js +187 -0
  26. package/dist/local-core/skill-sync.d.ts +132 -0
  27. package/dist/local-core/skill-sync.js +111 -0
  28. package/dist/local-core/skills.d.ts +10 -0
  29. package/dist/local-core/skills.js +49 -37
  30. package/dist/skills-client.d.ts +13 -2
  31. package/dist/skills-client.js +57 -5
  32. package/dist/skills-command.d.ts +1 -1
  33. package/dist/skills-command.js +152 -4
  34. package/dist/version.d.ts +1 -1
  35. package/dist/version.js +1 -1
  36. package/package.json +2 -2
@@ -1,9 +1,33 @@
1
- import { readFile } from "node:fs/promises";
2
- import { diffSkillBundles, installSkillBundle, readSkillBundle, scanLocalSkills, uninstallSkill, validateSkillBundle } from "./local-core/index.js";
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 === "scan") {
25
- print(await scanLocalSkills(args.rest.length > 1 ? args.rest.slice(1) : undefined));
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.15.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.15.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.15.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": ">=20"
54
+ "node": ">=22.13.0"
55
55
  },
56
56
  "exports": {
57
57
  ".": {