owfadev 0.3.0 → 0.3.1
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/.meta/VERSION +1 -1
- package/.meta/docs/packages.md +11 -1
- package/.meta/scripts/wiki_plugin_kernel.py +55 -10
- package/.meta/tests/test_install_preflight.py +177 -0
- package/README.md +2 -4
- package/README.zh-CN.md +103 -0
- package/bin/owfa.js +57 -3
- package/package.json +1 -1
package/.meta/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.3
|
|
1
|
+
0.3.1
|
package/.meta/docs/packages.md
CHANGED
|
@@ -82,7 +82,7 @@ uninstall: # declaration, never executable hooks
|
|
|
82
82
|
| `validate --scope <pkg>` | single-package tolerant mode (D3): deps absent outside the package degrade to warnings — the CI shape for package repos post-split, run with a framework checkout alongside |
|
|
83
83
|
| `pkg` | package inventory: members, connectors, implicit core remainder |
|
|
84
84
|
| `residual <pkg>` | D6 residual scan: greps handwritten AGENTS region + profile/language/calendar/todo/cron anchors for member references (the kernel-managed injection region is skipped — it is cleaned mechanically) |
|
|
85
|
-
| `install <pkg>[@tag]` | resolve the [registry](https://github.com/owfadev/registry) index (members reconciled against PKG.yaml) → trust gate → clone → copy masters/connectors/commands → converge. `install <owner/repo> --yes` installs an unindexed repository (trust: unknown); `--path <dir>` installs a local one (channel C) |
|
|
85
|
+
| `install <pkg>[@tag]` | resolve the [registry](https://github.com/owfadev/registry) index (members reconciled against PKG.yaml) → trust gate → clone → prospective manifest/budget preflight → copy masters/connectors/commands → converge. `install <owner/repo> --yes` installs an unindexed repository (trust: unknown); `--path <dir>` installs a local one (channel C) |
|
|
86
86
|
| `uninstall <pkg> --yes` | D6 four-clear (masters/commands/connectors/skills) + convergence + residual scan + retains/credentials reminders — data never touched |
|
|
87
87
|
| `list-remote` | index table with installed marks |
|
|
88
88
|
| `plan <pkg>` | dependency disclosure before a package lands |
|
|
@@ -91,6 +91,16 @@ uninstall: # declaration, never executable hooks
|
|
|
91
91
|
|
|
92
92
|
*Why scope mode exists before the split:* after the split the framework repo and each package repo no longer contain the full graph — validate would be blind exactly where it matters most. Absence-tolerance turns "plugin missing" from an error into a listed warning, keeping every repo self-checkable.
|
|
93
93
|
|
|
94
|
+
### Install failure handling
|
|
95
|
+
|
|
96
|
+
Before copying into the instance, `install` stages the prospective masters in a temporary directory using the actual install filter (including `--without` and its dependent-member pruning). It combines them with installed manifests and checks manifest/dependency rules and the existing UTF-8 injection budget. A rejection returns exit 1 before changing package masters, commands, connectors, migration state, journal or projections. Registry lookup may still refresh its local index cache; downloaded temporary sources are discarded on preflight rejection, while a caller's `--path` source is retained.
|
|
97
|
+
|
|
98
|
+
After copying, full binding/bridge/package validation and projection/deployment still run. Failure returns exit 1 with an incomplete-state warning; later projection steps stop when a preceding step returns failure. This is **not transactional rollback**: errors discovered after preflight, filesystem failures, or concurrent changes can leave files behind. Inspect the reported errors and run `kernel validate` before recovery; a successful diagnostic does not itself repair missing projection markers. Pending semantic init migrations retain the existing behavior: mechanical install can return 0 while explicitly reporting pending work, and `validate` stays red until the migration is completed and cleared.
|
|
99
|
+
|
|
100
|
+
The 16 KiB limit and its calculation are unchanged (#48/#49). This does not make `upgrade` transactional: its existing uninstall/reinstall sequence can remove the old package before the install preflight runs. Uninstall's own return-code contract is unchanged.
|
|
101
|
+
|
|
102
|
+
Offline regression suite: `python -m unittest discover -s .meta/tests -p test_install_preflight.py -v` (synthetic packages; no network or credentials).
|
|
103
|
+
|
|
94
104
|
## Uninstall contract (D6)
|
|
95
105
|
|
|
96
106
|
No executable uninstall hooks — that would be a **third execution surface** (after inject text and connectors), running at a privileged moment; the security model declines. Instead the package *declares* its deletion semantics (`uninstall:` above) and the flow is: kernel mechanical four-clear (directory / injection / registry / projections) → residual scan output → agent semantic finish per the declaration (credential reminders, process stops, bridge registration lines) behind a confirmation gate. **Data and credentials are never auto-deleted** — the freedom to delete belongs to the human; retained data zones lapse into the deprecated posture.
|
|
@@ -220,10 +220,10 @@ def _plugin_dirs(base):
|
|
|
220
220
|
yield sub2, s2dir
|
|
221
221
|
|
|
222
222
|
|
|
223
|
-
def load_plugins():
|
|
223
|
+
def load_plugins(base=None):
|
|
224
224
|
"""Read all plugin manifests (flat / core/ / domains/<pkg>/ layouts); returns (plugins, errors)."""
|
|
225
225
|
plugins, errors = {}, []
|
|
226
|
-
for name, pdir in _plugin_dirs(PLUGINS_DIR):
|
|
226
|
+
for name, pdir in _plugin_dirs(PLUGINS_DIR if base is None else base):
|
|
227
227
|
yml = os.path.join(pdir, "PLUGIN.yaml")
|
|
228
228
|
md = os.path.join(pdir, "PLUGIN.md")
|
|
229
229
|
if not os.path.exists(md):
|
|
@@ -990,11 +990,48 @@ def converge(label):
|
|
|
990
990
|
print(f"[{label}] validation failed ({len(errors)} errors) — fix before continuing")
|
|
991
991
|
return False
|
|
992
992
|
print(f"[{label}] validate: {len(plugins)} plugins green")
|
|
993
|
-
do_inject
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
do_deploy()
|
|
993
|
+
for project in (do_inject, do_check_inject, do_cmd_inject, do_registry):
|
|
994
|
+
if not project(plugins):
|
|
995
|
+
print(f"[{label}] projection failed ({project.__name__}) — state may be partial")
|
|
996
|
+
return False
|
|
997
|
+
if not do_deploy():
|
|
998
|
+
print(f"[{label}] deployment failed — state may be partial")
|
|
999
|
+
return False
|
|
1000
|
+
return True
|
|
1001
|
+
|
|
1002
|
+
|
|
1003
|
+
def _preflight_install(src, ignore):
|
|
1004
|
+
"""Validate the prospective manifest graph before writing installed package state.
|
|
1005
|
+
|
|
1006
|
+
Stage only masters using the same copy filter as install, including --without.
|
|
1007
|
+
Binding/bridge/package checks still run after commands and connectors land.
|
|
1008
|
+
This is a preflight, not a transaction or a replacement for convergence.
|
|
1009
|
+
"""
|
|
1010
|
+
import tempfile
|
|
1011
|
+
|
|
1012
|
+
plugins, errors = load_plugins()
|
|
1013
|
+
warns = []
|
|
1014
|
+
try:
|
|
1015
|
+
with tempfile.TemporaryDirectory(prefix="vw-preflight-") as stage:
|
|
1016
|
+
shutil.copytree(src, os.path.join(stage, "package"), ignore=ignore)
|
|
1017
|
+
incoming, load_errors = load_plugins(stage)
|
|
1018
|
+
errors.extend(load_errors)
|
|
1019
|
+
for name, manifest in incoming.items():
|
|
1020
|
+
if name in plugins:
|
|
1021
|
+
errors.append(f"[error] {name}/: duplicate plugin id (already installed)")
|
|
1022
|
+
else:
|
|
1023
|
+
plugins[name] = manifest
|
|
1024
|
+
if not errors:
|
|
1025
|
+
validate(plugins, errors, warnings=warns)
|
|
1026
|
+
except (OSError, ValueError) as e:
|
|
1027
|
+
errors.append(f"[error] install preflight: {e}")
|
|
1028
|
+
for w in warns:
|
|
1029
|
+
print(f"[warning] {w}")
|
|
1030
|
+
for e in errors:
|
|
1031
|
+
print(e)
|
|
1032
|
+
if errors:
|
|
1033
|
+
print("[install] preflight failed — package files and projections were not changed")
|
|
1034
|
+
return False
|
|
998
1035
|
return True
|
|
999
1036
|
|
|
1000
1037
|
|
|
@@ -1110,9 +1147,14 @@ def do_install(spec, args, prev_version=None):
|
|
|
1110
1147
|
if os.path.exists(dst):
|
|
1111
1148
|
print(f"[error] {pid} already installed — uninstall first (kernel uninstall {pid} --yes)")
|
|
1112
1149
|
return 1
|
|
1150
|
+
ignore = shutil.ignore_patterns(".git", ".meta", "connectors", "command", "README.md",
|
|
1151
|
+
"__pycache__", "migrations", *sorted(pruned))
|
|
1152
|
+
if not _preflight_install(src, ignore):
|
|
1153
|
+
if not path:
|
|
1154
|
+
shutil.rmtree(src, ignore_errors=True)
|
|
1155
|
+
return 1
|
|
1113
1156
|
os.makedirs(os.path.dirname(dst), exist_ok=True)
|
|
1114
|
-
shutil.copytree(src, dst, ignore=
|
|
1115
|
-
*sorted(pruned)))
|
|
1157
|
+
shutil.copytree(src, dst, ignore=ignore)
|
|
1116
1158
|
copied_conn, copied_cmd = [], []
|
|
1117
1159
|
csrc = os.path.join(src, "connectors")
|
|
1118
1160
|
if os.path.isdir(csrc):
|
|
@@ -1163,9 +1205,12 @@ def do_install(spec, args, prev_version=None):
|
|
|
1163
1205
|
+ (f"; connectors: {', '.join(copied_conn)}" if copied_conn else "")
|
|
1164
1206
|
+ (f"; commands: {', '.join(copied_cmd)}" if copied_cmd else ""))
|
|
1165
1207
|
_journal("install", f"{pid} v{pkgm.get('version')}" + (f" subset(-{','.join(sorted(pruned))})" if pruned else "") + (f" staged={','.join(staged)}" if staged else ""))
|
|
1166
|
-
converge("install")
|
|
1208
|
+
converged = converge("install")
|
|
1167
1209
|
if staged:
|
|
1168
1210
|
print(f"[pending] semantic migration staged for {pid}: {', '.join(staged)} — kernel migrate {pid}; validate stays red until cleared")
|
|
1211
|
+
if not converged:
|
|
1212
|
+
print("[error] install incomplete — files may remain; run kernel validate before retrying")
|
|
1213
|
+
return 1
|
|
1169
1214
|
return 0
|
|
1170
1215
|
|
|
1171
1216
|
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""Offline install regressions; synthetic packages and disposable instances only.
|
|
2
|
+
|
|
3
|
+
Run: python -m unittest discover -s .meta/tests -p test_install_preflight.py -v
|
|
4
|
+
"""
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
import shutil
|
|
10
|
+
import subprocess
|
|
11
|
+
import sys
|
|
12
|
+
import tempfile
|
|
13
|
+
import unittest
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
FRAMEWORK = Path(__file__).resolve().parents[2]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class InstallPreflightTests(unittest.TestCase):
|
|
20
|
+
def setUp(self):
|
|
21
|
+
self.temp = tempfile.TemporaryDirectory(prefix="kernel-install-test-")
|
|
22
|
+
self.addCleanup(self.temp.cleanup)
|
|
23
|
+
self.root = Path(self.temp.name) / "instance"
|
|
24
|
+
self.root.mkdir()
|
|
25
|
+
for directory in (".meta", ".agents"):
|
|
26
|
+
shutil.copytree(FRAMEWORK / directory, self.root / directory,
|
|
27
|
+
ignore=shutil.ignore_patterns("__pycache__", "tests", "smoke-tmp"))
|
|
28
|
+
(self.root / "AGENTS.md").write_text(
|
|
29
|
+
"# Synthetic instance\n<!-- wiki-inject:start -->\n<!-- wiki-inject:end -->\n",
|
|
30
|
+
encoding="utf-8")
|
|
31
|
+
self.source = Path(self.temp.name) / "source"
|
|
32
|
+
self.source.mkdir()
|
|
33
|
+
self.env = dict(os.environ, PYTHONDONTWRITEBYTECODE="1", PYTHONIOENCODING="utf-8")
|
|
34
|
+
self.env.pop("PYTHONPATH", None)
|
|
35
|
+
result = self.run_kernel("all")
|
|
36
|
+
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
|
37
|
+
|
|
38
|
+
def run_kernel(self, *args):
|
|
39
|
+
return subprocess.run(
|
|
40
|
+
[sys.executable, str(self.root / ".meta/scripts/wiki_plugin_kernel.py"), *args],
|
|
41
|
+
cwd=self.root, env=self.env, capture_output=True, text=True, encoding="utf-8")
|
|
42
|
+
|
|
43
|
+
def run_code(self, code):
|
|
44
|
+
prefix = "import sys; sys.path.insert(0, '.meta/scripts'); import wiki_plugin_kernel as k; "
|
|
45
|
+
return subprocess.run([sys.executable, "-c", prefix + code], cwd=self.root,
|
|
46
|
+
env=self.env, capture_output=True, text=True, encoding="utf-8")
|
|
47
|
+
|
|
48
|
+
def manifest(self, name, inject, depends="[domain, wiki, trust, log]", tier=None):
|
|
49
|
+
directory = self.source / name
|
|
50
|
+
directory.mkdir(exist_ok=True)
|
|
51
|
+
dependencies = "".join(f" - {item.strip()}\n" for item in depends.strip("[]").split(","))
|
|
52
|
+
text = (f"id: {name}\nschema_version: 1\nversion: 0.1\nupdated: 2026-10-24\n"
|
|
53
|
+
f"depends:\n{dependencies}attachment: []\nfields:\n"
|
|
54
|
+
f"inject: {json.dumps(inject, ensure_ascii=False)}\n")
|
|
55
|
+
if tier:
|
|
56
|
+
text += f"inject_tier: {tier}\n"
|
|
57
|
+
(directory / "PLUGIN.yaml").write_text(text, encoding="utf-8")
|
|
58
|
+
(directory / "PLUGIN.md").write_text("# Synthetic test plugin\n", encoding="utf-8")
|
|
59
|
+
|
|
60
|
+
def package(self, inject="Synthetic test", members="[probe]"):
|
|
61
|
+
(self.source / "PKG.yaml").write_text(
|
|
62
|
+
"schema_version: 1\nid: probe\nversion: 0.1\ncompat: '>= 0.3'\n"
|
|
63
|
+
f"members: {members}\ndepends: [framework]\n", encoding="utf-8")
|
|
64
|
+
self.manifest("probe", inject)
|
|
65
|
+
|
|
66
|
+
def install(self, *extra):
|
|
67
|
+
return self.run_kernel("install", "probe", "--path", str(self.source), *extra)
|
|
68
|
+
|
|
69
|
+
def snapshot(self):
|
|
70
|
+
return {str(path.relative_to(self.root)): hashlib.sha256(path.read_bytes()).hexdigest()
|
|
71
|
+
for path in self.root.rglob("*") if path.is_file()}
|
|
72
|
+
|
|
73
|
+
def budget_text(self, extra=0):
|
|
74
|
+
result = self.run_code(
|
|
75
|
+
"p,e=k.load_plugins(); assert not e; b,_=k.build_inject_blocks(p); "
|
|
76
|
+
"print(k.INJECT_BUDGET-sum(len(x.encode('utf-8')) for x in b))")
|
|
77
|
+
self.assertEqual(result.returncode, 0, result.stderr)
|
|
78
|
+
overhead = len("<!-- plugin:probe v0.1 -->\n- \n<!-- /plugin:probe -->".encode("utf-8"))
|
|
79
|
+
room = int(result.stdout.strip()) - overhead + extra
|
|
80
|
+
return "界" * (room // 3) + "x" * (room % 3)
|
|
81
|
+
|
|
82
|
+
def test_over_budget_leaves_all_instance_state_unchanged(self):
|
|
83
|
+
self.package(self.budget_text(extra=1))
|
|
84
|
+
# These would all land or change if rejection happened after copy/staging.
|
|
85
|
+
for relative in ("connectors/test-connector/file.txt", "command/test-command/file.txt",
|
|
86
|
+
"migrations/init.md"):
|
|
87
|
+
path = self.source / relative
|
|
88
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
89
|
+
path.write_text("Synthetic payload", encoding="utf-8")
|
|
90
|
+
ledger = self.root / ".meta/migrations/pending.json"
|
|
91
|
+
ledger.parent.mkdir(exist_ok=True)
|
|
92
|
+
ledger.write_text(json.dumps({"probe": {"kind": "teardown", "items": []}}), encoding="utf-8")
|
|
93
|
+
before = self.snapshot()
|
|
94
|
+
result = self.install()
|
|
95
|
+
self.assertEqual(result.returncode, 1, result.stdout + result.stderr)
|
|
96
|
+
self.assertIn("over the 16384 B budget", result.stdout)
|
|
97
|
+
self.assertIn("preflight failed", result.stdout)
|
|
98
|
+
self.assertEqual(self.snapshot(), before)
|
|
99
|
+
self.assertTrue((self.source / "probe/PLUGIN.yaml").exists())
|
|
100
|
+
|
|
101
|
+
def test_exact_utf8_budget_installs_and_converges(self):
|
|
102
|
+
self.package(self.budget_text())
|
|
103
|
+
result = self.install()
|
|
104
|
+
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
|
105
|
+
self.assertIn("plugin:probe", (self.root / "AGENTS.md").read_text(encoding="utf-8"))
|
|
106
|
+
result = self.run_kernel("all")
|
|
107
|
+
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
|
108
|
+
|
|
109
|
+
def test_without_cascade_uses_only_the_installed_subset(self):
|
|
110
|
+
self.package(members="[probe, optional, child]")
|
|
111
|
+
self.manifest("optional", "界" * 6000, depends="[probe]")
|
|
112
|
+
self.manifest("child", "界" * 6000, depends="[optional]")
|
|
113
|
+
result = self.install("--without", "optional")
|
|
114
|
+
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
|
115
|
+
installed = self.root / ".meta/plugins/domains/probe"
|
|
116
|
+
self.assertFalse((installed / "optional").exists())
|
|
117
|
+
self.assertFalse((installed / "child").exists())
|
|
118
|
+
self.assertEqual(self.run_kernel("validate").returncode, 0)
|
|
119
|
+
|
|
120
|
+
def test_member_tier_does_not_consume_injection_budget(self):
|
|
121
|
+
self.package(members="[probe, member]")
|
|
122
|
+
self.manifest("member", "界" * 6000, depends="[probe]", tier="member")
|
|
123
|
+
result = self.install()
|
|
124
|
+
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
|
125
|
+
self.assertNotIn("plugin:member", (self.root / "AGENTS.md").read_text(encoding="utf-8"))
|
|
126
|
+
|
|
127
|
+
def test_duplicate_installed_id_is_rejected_before_copy(self):
|
|
128
|
+
self.package(members="[probe, language]")
|
|
129
|
+
self.manifest("language", "Duplicate", depends="[probe]")
|
|
130
|
+
before = self.snapshot()
|
|
131
|
+
result = self.install()
|
|
132
|
+
self.assertEqual(result.returncode, 1, result.stdout + result.stderr)
|
|
133
|
+
self.assertIn("duplicate plugin id", result.stdout)
|
|
134
|
+
self.assertEqual(self.snapshot(), before)
|
|
135
|
+
|
|
136
|
+
def test_post_copy_binding_failure_returns_nonzero_and_warns(self):
|
|
137
|
+
self.package()
|
|
138
|
+
path = self.source / "probe/PLUGIN.yaml"
|
|
139
|
+
path.write_text(path.read_text(encoding="utf-8") + "commands:\n - missing-command\n", encoding="utf-8")
|
|
140
|
+
result = self.install()
|
|
141
|
+
self.assertEqual(result.returncode, 1, result.stdout + result.stderr)
|
|
142
|
+
self.assertIn("install incomplete", result.stdout)
|
|
143
|
+
self.assertIn("missing command", result.stdout)
|
|
144
|
+
self.assertTrue((self.root / ".meta/plugins/domains/probe/PKG.yaml").exists())
|
|
145
|
+
|
|
146
|
+
def test_projection_failure_returns_nonzero(self):
|
|
147
|
+
self.package()
|
|
148
|
+
(self.root / "AGENTS.md").write_text("# Missing injection markers\n", encoding="utf-8")
|
|
149
|
+
result = self.install()
|
|
150
|
+
self.assertEqual(result.returncode, 1, result.stdout + result.stderr)
|
|
151
|
+
self.assertIn("projection failed (do_inject)", result.stdout)
|
|
152
|
+
self.assertIn("install incomplete", result.stdout)
|
|
153
|
+
|
|
154
|
+
def test_semantic_init_stays_pending_without_becoming_mechanical_failure(self):
|
|
155
|
+
self.package()
|
|
156
|
+
migration = self.source / "migrations/init.md"
|
|
157
|
+
migration.parent.mkdir()
|
|
158
|
+
migration.write_text("# Synthetic manual initialization\n", encoding="utf-8")
|
|
159
|
+
result = self.install()
|
|
160
|
+
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
|
161
|
+
self.assertIn("semantic migration staged", result.stdout)
|
|
162
|
+
result = self.run_kernel("validate")
|
|
163
|
+
self.assertEqual(result.returncode, 1, result.stdout + result.stderr)
|
|
164
|
+
self.assertIn("pending semantic migration", result.stdout)
|
|
165
|
+
|
|
166
|
+
def test_deploy_failure_is_propagated(self):
|
|
167
|
+
self.package()
|
|
168
|
+
result = self.run_code(
|
|
169
|
+
"k.do_deploy=lambda: False; "
|
|
170
|
+
f"sys.exit(k.do_install('probe', ['--path', {str(self.source)!r}]))")
|
|
171
|
+
self.assertEqual(result.returncode, 1, result.stdout + result.stderr)
|
|
172
|
+
self.assertIn("deployment failed", result.stdout)
|
|
173
|
+
self.assertIn("install incomplete", result.stdout)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
if __name__ == "__main__":
|
|
177
|
+
unittest.main()
|
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# OWFA — One Workspace For All (agents)
|
|
2
2
|
|
|
3
|
+
> English | [简体中文](README.zh-CN.md)
|
|
4
|
+
|
|
3
5
|
> **OWFA** is the project; **OWFA Core** is the framework body (formerly `vault-wiki`); **OWFA CLI** is the installer. The public home is moving to **`owfadev/owfa`** — `github.com/owfa` is an unrelated organization. The npm name `owfa` is under application; see [The name](#the-name).
|
|
4
6
|
|
|
5
7
|
**OWFA** (One Workspace For All agents) is an open-source agent workspace: out-of-the-box on first install, continuously maintainable over time, and pluginized end to end. One workspace — the directory you own, holding `vault/` (real assets), `wiki/` (md proxies and native notes) and `AGENTS.md` (the constitution) — is the standing context that every agent you use re-enters instead of being re-introduced each session. The agent reads and writes with zero priors per the SASU-L disclosure order; md plus plain files are the foundation, and Obsidian and friends are replaceable viewers. Behavior is personalizable through a living user profile, and new capabilities land as plugins — every plugin ships through the registry with a human-review gate, so contributions are welcome: build a domain plugin for the sources you use and file a one-line PR to list it (see [Contributing](#contributing)).
|
|
@@ -96,10 +98,6 @@ While the name is under application, substitute `npx owfadev` for every `npx owf
|
|
|
96
98
|
- [.meta/docs/quickstart.md](https://github.com/owfadev/owfa/blob/master/.meta/docs/quickstart.md) — five-minute path: one command or the manual five steps, then first run
|
|
97
99
|
- `.meta/protocol/` — field registry, action discipline, disclosure paradigm
|
|
98
100
|
|
|
99
|
-
## History
|
|
100
|
-
|
|
101
|
-
2026-08-26 founded as a copy of the personal library's structure; 08-28 repositioned as this project; 09-08 the plugin-plus-command prototype landed directly, validated by real operation then frozen. 09-12 refactor: the conceptual pair wiki & vault established; the original vault plugin renamed mapping; layering abolished; identifiers anglicized. 09-19 the lark family and the calendar time territory established. 09-22 domainization: the domain conceptual root established; vault, lark, project compliant as instances. 09-29 profile became a standalone command; the email domain established; the connector slot established. 10-01 to 10-02 the bb domain suite landed and was validated across four courses; collaboration red lines set; converted to a team project. 10-04 the bb cognition-consumer refactor — bb-teach, bb-quiz, bb-track commands; mechanics docs opened; docs moved to `.meta/docs/`. 10-05 the cuhksz school domain established, bb demoted into the family, sis and registry sub-domains followed. 10-06 the bilibili domain established (query-and-answer + emergence archives, low-risk write whitelist), connector bili-cli first version; the cron time-automation territory established (declaration-as-source, one page per task), lark-calendar/bili/email migrated onto the bridge; bili 0.3 video-archival batch. Docs switched to English. Design-lineage discussions live in the personal library — see AGENTS.md pointers. 10-12 repo transferred to the GitHub org AIE3905-Wuwa; standardization-and-split improvement phase opened: package binding (directory == package, PKG.yaml reconciliation, scope validate, residual scan, VERSION 0.2 anchor + tag v0.2); the convergence record lives in issue #20. 10-13 discovery/download/update shipped (registry INDEX, owner/repo trust gate, silent-reinstall upgrades, --without subsets, VERSION 0.3 + tag v0.3), then teardown (x->0) closed the migration algebra (dormant staging, tri-face warning, residual gate, sweep symmetry); five packages re-released at v0.2; a fresh-instance acceptance run walked the full lifecycle green and fed back the cold-start doc fixes; the sandbox left git tracking (local-only, owner re-ruling); quickstart gained the two postures (clone-as-instance for users, template + instance for developers).
|
|
102
|
-
|
|
103
101
|
## License
|
|
104
102
|
|
|
105
103
|
MIT — see [LICENSE](https://github.com/owfadev/owfa/blob/master/LICENSE).
|
package/README.zh-CN.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# OWFA — One Workspace For All (agents)
|
|
2
|
+
|
|
3
|
+
> [English](README.md) | 简体中文
|
|
4
|
+
|
|
5
|
+
> **OWFA** 是项目名;**OWFA Core** 是框架本体(原 `vault-wiki`);**OWFA CLI** 是安装器。公开主页为 **`owfadev/owfa`**——`github.com/owfa` 是无关组织。npm 包名 `owfa` 正在申请中,详见[「关于命名」](#关于命名)。
|
|
6
|
+
|
|
7
|
+
**OWFA**(One Workspace For All agents)是一个开源的 agent 工作区:开箱即用、可持续维护、端到端插件化。一个工作区——你拥有的那个目录,装着 `vault/`(真实资产)、`wiki/`(md 投影与原生笔记)和 `AGENTS.md`(章程)——是你所有 agent 反复进入的常驻上下文,而不是每次会话重新自我介绍。agent 按 SASU-L 披露次序零先验地读写;md 与纯文本文件是地基,Obsidian 等只是可替换的查看器。行为通过一份活的用户画像个性化,新能力以插件落地——每个插件都经 registry 的人工审核门发布,欢迎贡献:为你使用的信息源写一个域插件,向 registry 提一行 PR 即可上架(见[参与贡献](#参与贡献))。
|
|
8
|
+
|
|
9
|
+
> 状态:**alpha**——能力面是经六周真实日常使用打磨出的原型(生于 2026-09-08,经两轮实际运营与一次冷启动披露审计检验,bb 课程家族在四门课真实运行);分发面 v0.3 是首个可安装形态——经一次全新实例验收,尚无外部用户。Beta 门槛:行为契约(#34)落地 · 两位成员真实装机 · 旧实例转换走通 · 验收脚本可重放绿。`.meta/VERSION` 锚定的是内核契约而非产品成熟度——阶段标签在这里与 release notes。2026-10-02 转为团队项目。文档语言 2026-10-06 起切换为英文。2026-10-25:框架本体有了自己的分发通道——npm CLI `owfa`(`init` / `upgrade` / `doctor` / `add`),补上「clone + 拷两棵树 + 手写 AGENTS.md」的缺口(issues #45/#46);npm 名 `owfa` 因相似性策略冻结,申请中,`owfadev` 为兜底命名空间。
|
|
10
|
+
|
|
11
|
+
## 关于命名
|
|
12
|
+
|
|
13
|
+
**OWFA — One Workspace For All (agents)。** 一个工作区——你拥有的目录,装着 `vault/`、`wiki/`、`AGENTS.md` 与框架本体——你使用的每一个 agent 都重新进入它,而不是每次会话重新认识它。*All* 指的是*你的*所有 agent,不是所有人,也不是托管服务;人始终是所有者与审计锚点。`vault-wiki` 退出公开面:公开文案称 **OWFA**(或 **OWFA Core**),GitHub 家在 **`owfadev/owfa`**,CLI 以 `owfa` 之名发布。npm 地址尚未空闲——npm 以「与 `ow`/`ora` 过于相似」冻结了 `owfa`,申诉进行中,`owfadev` 为兜底包。完整的理据、分层与撞名对策:[`.meta/docs/naming.md`](https://github.com/owfadev/owfa/blob/master/.meta/docs/naming.md)。
|
|
14
|
+
|
|
15
|
+
## 它做什么
|
|
16
|
+
|
|
17
|
+
让 AI 编码 agent 替你经营一座「个人 wiki」:
|
|
18
|
+
|
|
19
|
+
- 资产随手丢进 `vault/`,任何格式——md、txt、csv、pdf、图片。**map** 命令把每件资产登记为 `wiki/vault/` 下的 md 投影页,带 SHA-256、元数据与链接
|
|
20
|
+
- 对话中的洞见与决策由 **save** 命令蒸馏为 `wiki/notes/` 下的原生笔记;会话骨架页进 `wiki/sessions/`
|
|
21
|
+
- **query** 命令先读热缓存与索引,再以 wikilink 引用作答
|
|
22
|
+
- 索引、标签、热缓存与运行日志由派生层自动维护;**check** 命令体检库健康;**plugin** 命令装卸结构插件
|
|
23
|
+
- 外部信息源以域接入:lark 与 email 落为指针或归档页;calendar 管时间线;project 管项目容器;bb 经 bb-cli 拉课件、作业与成绩;bilibili 经 bili-cli 按需应答(搜索/详情/收藏/UP 追更;视频摘要与评审经显式请求归档;低风险写白名单须用户显式动词)。翻译发生在域外;域内一切互链
|
|
24
|
+
- **profile** 命令维护一份关于用户的活认知档案:断言带证据,偏好会过期;任何个性化决策前先读它
|
|
25
|
+
- **teach** 命令按认知档案讲解——熟点略讲或抽测,生点拆解;重要问答沉淀进 ai notes
|
|
26
|
+
- **quiz** 命令按范围加可选样本出题;批改结果回流认知档案
|
|
27
|
+
- **track** 命令读写每门课的 user.md 学习状态;teach 与 quiz 的使用挂载于此
|
|
28
|
+
|
|
29
|
+
## 核心概念
|
|
30
|
+
|
|
31
|
+
| 概念 | 定义 |
|
|
32
|
+
|---|---|
|
|
33
|
+
| wiki | md 世界,内外之分的内侧:各域投影 + 原生笔记 + 派生层 |
|
|
34
|
+
| domain | wiki 之外信息源的适配器契约。vault 是默认域;lark、project、email、bb、bili 也是域 |
|
|
35
|
+
| vault | 默认域——真实资产仓库;命令侧只增不删,删除与修改的自由属于人 |
|
|
36
|
+
| SASU-L | 披露范式:agent 只经系统提示、AGENTS.md、Skills、用户的话与循环来学习 |
|
|
37
|
+
|
|
38
|
+
## 目录结构
|
|
39
|
+
|
|
40
|
+
| 目录 | 内容 |
|
|
41
|
+
|------|------|
|
|
42
|
+
| `.meta/` | 原型核心,两级插件布局:`plugins/core/`(20 个框架插件——domain 与 wiki 两个概念根、横切件,以及 vault/mapping/structure 的明示章程例外)+ `plugins/domains/<pkg>/`(一包一目录:`PKG.yaml` + 家族母本——cuhksz 8、lark 4、email、bili、project)。目录边界 == 层级边界 == 包边界;`PKG.yaml` 声明名册,内核对账家族闭包;`.meta/VERSION` 锚定框架版本(包 `compat` 下限对它校验)。注入次序 = 依赖拓扑 + 字典序;AGENTS 注入区分层——成员级域件退出注入区(家族名册 + 技能 + 按需 manifest 读取承担曝光),受内核字节预算约束;全局插件可声明桥,必依桥由内核校验。另有 13 个命令母本、协议工件(registry、actions、experiments)与机械脚本 wiki_plugin_kernel、pipeline、wikilib——纯标准库零依赖 |
|
|
43
|
+
| `connectors/` | 连接器母本:部署侧 CLI 事实接口 + 技能使用披露。`connectors/*/SKILL.md` 经内核 deploy 落入 `.agents/skills/`;现有 bb-cli、sis-cli、mail-cli、bili-cli。连接器随各自包的 `PKG.yaml` 走 |
|
|
44
|
+
| Packages | 索引在 [registry](https://github.com/owfadev/registry)(Obsidian 模式:包自托管于各自仓,一行 PR 即上架门)。`kernel install`/`list-remote` 对它解析;未入索引的仓 `--yes` 可装(trust: unknown)。组织仓 `pkg-cuhksz` / `pkg-lark` / `pkg-email` / `pkg-bili` / `pkg-project` / `pkg-github` 承载各包,带完整文件谱系拆分 |
|
|
45
|
+
| `.agents/skills/` | 命令与连接器技能的部署副本(拆分后框架命令在此;连接器技能随包到达) |
|
|
46
|
+
| `bin/` + `templates/` + `package.json` | npm 通道:`owfa` CLI、AGENTS 种子、包清单。`test/` 与 `scripts/` 仅供开发,不随包发布 |
|
|
47
|
+
| `test-repo/` | 本地测试沙盒——2026-10-13 起不入 git(owner 裁定;全新 clone 不含它)。需要 AGENTS.md 上下文的实验用临时部署;设置与规则见 [`.meta/docs/sandbox.md`](https://github.com/owfadev/owfa/blob/master/.meta/docs/sandbox.md)。框架仓自身不带 AGENTS.md——库姿态;开发规则在 [CONTRIBUTING.md](https://github.com/owfadev/owfa/blob/master/CONTRIBUTING.md) |
|
|
48
|
+
| `.meta/docs/` | 人读文档:`intro.md` 导论、`mechanics.md` 机制、`usage.md` 使用指南、`deployment.md` 全指南 / `quickstart.md` 五分钟路径、`instance-git.md` 实例侧 git 最小契约、`research-*.md` 研究文件——文末直链。权威机制在内核参考 skill;文档不复写机制 |
|
|
49
|
+
|
|
50
|
+
## 参与贡献
|
|
51
|
+
|
|
52
|
+
欢迎贡献,插件是预定贡献面:新能力是新插件,不是 fork 框架。三条路径,由小到大——为你使用的 CLI 写一个连接器技能;写一个把外部信息源适配进 wiki 的域插件(三路径表与开发循环见 [`developer.md`](https://github.com/owfadev/owfa/blob/master/.meta/docs/developer.md));或改进 core。域包自托管于各自仓;上架 = 向 [registry](https://github.com/owfadev/registry) 提一行 PR,那是人工审核的信任门。Bug 与行为问题走 issues;框架本体收 PR。
|
|
53
|
+
|
|
54
|
+
## 协作
|
|
55
|
+
|
|
56
|
+
本仓是团队项目:三名成员有写权限,一切经 PR(1 个 approving review 可合,`enforce_admins` 开启);owner 对仓库设置、权限与章程/内核变更有最终决定权。数据红线、提交流程与版本纪律 summarized 在 [CONTRIBUTING.md](https://github.com/owfadev/owfa/blob/master/CONTRIBUTING.md)——权威开发规则;变更轨迹是 git 历史 + PR + issues(本仓不留运行日志)。
|
|
57
|
+
|
|
58
|
+
## 快速开始
|
|
59
|
+
|
|
60
|
+
两条入口。**通道(推荐):** `npx owfa init my-workspace`——安装器要 Node 16.7+,Core 要 Python 3.9+;无需 clone(npm 名申请期间用 `npx owfadev init` 兜底)。**手动:** clone 本仓并按 [quickstart.md](https://github.com/owfadev/owfa/blob/master/.meta/docs/quickstart.md) 的五步走,然后 `python .meta/scripts/wiki_plugin_kernel.py all`。含包与升级的全指南:[deployment.md](https://github.com/owfadev/owfa/blob/master/.meta/docs/deployment.md)。
|
|
61
|
+
|
|
62
|
+
前提:Python 3.9+,纯标准库,框架本身免安装(连接器自带依赖);仅 npm 通道要 Node 16.7+;推荐 git(实例快照);一个会读 AGENTS.md 与 skills 的 agent 环境,如 ZCode;Obsidian 可选,仅查看器。
|
|
63
|
+
|
|
64
|
+
在仓库根开一个 agent 会话。AGENTS.md 是章程,含插件注入区;十三个命令经自然语言触发:**map / save / profile / query / check / plugin / lark-map / bb-map / asset-read / 内核参考 / teach / quiz / track**——map、save、profile、query、check、plugin、lark-map、bb-map、asset-read、wiki_plugin_kernel、bb-teach、bb-quiz、bb-track;母本在 `.meta/command/`。丢一个文件进 `vault/`,对 agent 说「map」——这就是第一次使用。
|
|
65
|
+
|
|
66
|
+
## 部署
|
|
67
|
+
|
|
68
|
+
OWFA 有三层:**OWFA CLI**(安装器,绝不拷入工作区)、**OWFA Core**(`.meta/` + `.agents/skills/`,被安装之物)与 **kernel**(Core 内的纯标准库 Python 脚本——没有单独的内核安装;Python 3.9+ 即可运行)。`.meta/plugins/domains/<pkg>/` 下的域包是可选的。
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
npx owfa init my-workspace # 目标名;申请期间用 npx owfadev init
|
|
72
|
+
cd my-workspace
|
|
73
|
+
npx owfa doctor # 体检:python / 框架 / 标记 / kernel validate(兜底用 npx owfadev doctor)
|
|
74
|
+
npx owfa list # registry 目录
|
|
75
|
+
npx owfa add cuhksz # 可选包
|
|
76
|
+
npx owfa upgrade # 日后:替换框架树,保留实例状态
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
名字申请期间,把上面每条 `npx owfa` 换成 `npx owfadev` 即可(或全局安装任一包;两者都暴露 `owfa` 命令)。
|
|
80
|
+
|
|
81
|
+
`init` 是加法的:既有内容不动,已有 `AGENTS.md` 保留、只追加注入区。`upgrade` 替换 `.meta/` 与 `.agents/skills/`(实例数据永不触碰),保留内核迁移台账、journal 与 registry 缓存。
|
|
82
|
+
|
|
83
|
+
- 全部署指南(组件、通道、包、升级、故障):[`.meta/docs/deployment.md`](https://github.com/owfadev/owfa/blob/master/.meta/docs/deployment.md)
|
|
84
|
+
- 五分钟路径:[`.meta/docs/quickstart.md`](https://github.com/owfadev/owfa/blob/master/.meta/docs/quickstart.md)
|
|
85
|
+
- 通道设计与发布手册:[`.meta/docs/distribution.md`](https://github.com/owfadev/owfa/blob/master/.meta/docs/distribution.md)
|
|
86
|
+
|
|
87
|
+
## 文档索引
|
|
88
|
+
|
|
89
|
+
- [.meta/docs/deployment.md](https://github.com/owfadev/owfa/blob/master/.meta/docs/deployment.md) — 部署指南:组件、通道、首跑、包、升级、故障
|
|
90
|
+
- [.meta/docs/naming.md](https://github.com/owfadev/owfa/blob/master/.meta/docs/naming.md) — 命名:本义展开、每个词的承诺、两名两职、异议与撞名对策
|
|
91
|
+
- [.meta/docs/distribution.md](https://github.com/owfadev/owfa/blob/master/.meta/docs/distribution.md) — npm 通道:命令、工件、版本不变量、发布手册、安全姿态
|
|
92
|
+
- [.meta/docs/packages.md](https://github.com/owfadev/owfa/blob/master/.meta/docs/packages.md) — 包机制与设计理据:布局、PKG.yaml、不变量、版本、卸载契约、决策台账
|
|
93
|
+
- [.meta/docs/developer.md](https://github.com/owfadev/owfa/blob/master/.meta/docs/developer.md) — 开发者指南:插件落位三路径表、开发循环、注册与发现
|
|
94
|
+
- [.meta/docs/agents-example.md](https://github.com/owfadev/owfa/blob/master/.meta/docs/agents-example.md) / [.meta/docs/log-example.md](https://github.com/owfadev/owfa/blob/master/.meta/docs/log-example.md) — AGENTS.md 与 log.md 的实例种子(框架仓自身两者都不带)
|
|
95
|
+
- [.meta/docs/intro.md](https://github.com/owfadev/owfa/blob/master/.meta/docs/intro.md) — 导论:为什么是这个框架,叙事与谱系
|
|
96
|
+
- [.meta/docs/mechanics.md](https://github.com/owfadev/owfa/blob/master/.meta/docs/mechanics.md) — 机制:每个机制再深一层,附 bb 家族走查;动框架前先读
|
|
97
|
+
- [.meta/docs/usage.md](https://github.com/owfadev/owfa/blob/master/.meta/docs/usage.md) — 使用指南:三种工作循环(库务、课程学习、协作开发)
|
|
98
|
+
- [.meta/docs/quickstart.md](https://github.com/owfadev/owfa/blob/master/.meta/docs/quickstart.md) — 五分钟路径:一条命令或手动五步,然后首跑
|
|
99
|
+
- `.meta/protocol/` — 字段注册表、行动纪律、披露范式
|
|
100
|
+
|
|
101
|
+
## 许可
|
|
102
|
+
|
|
103
|
+
MIT——见 [LICENSE](https://github.com/owfadev/owfa/blob/master/LICENSE)。
|
package/bin/owfa.js
CHANGED
|
@@ -62,8 +62,10 @@ function rel(root, p) {
|
|
|
62
62
|
return path.relative(root, p) || ".";
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
const FLAG_VALUE = ["name", "packages", "without", "from", "path"];
|
|
66
|
+
|
|
65
67
|
function parseArgs(argv) {
|
|
66
|
-
const args = { _: [], flags: new Set() };
|
|
68
|
+
const args = { _: [], flags: new Set(), used: new Set() };
|
|
67
69
|
for (let i = 0; i < argv.length; i++) {
|
|
68
70
|
const a = argv[i];
|
|
69
71
|
if (a === "--") {
|
|
@@ -72,7 +74,8 @@ function parseArgs(argv) {
|
|
|
72
74
|
}
|
|
73
75
|
if (a.startsWith("--")) {
|
|
74
76
|
const [key, inline] = a.slice(2).split("=", 2);
|
|
75
|
-
const takesValue =
|
|
77
|
+
const takesValue = FLAG_VALUE.includes(key);
|
|
78
|
+
args.used.add(key);
|
|
76
79
|
if (takesValue && inline === undefined) {
|
|
77
80
|
args[key] = argv[++i];
|
|
78
81
|
} else if (takesValue) {
|
|
@@ -80,6 +83,8 @@ function parseArgs(argv) {
|
|
|
80
83
|
} else {
|
|
81
84
|
args.flags.add(key);
|
|
82
85
|
}
|
|
86
|
+
} else if (a.startsWith("-") && a !== "-") {
|
|
87
|
+
args.used.add(a.slice(1)); // short or mistyped flag — validated by the command
|
|
83
88
|
} else {
|
|
84
89
|
args._.push(a);
|
|
85
90
|
}
|
|
@@ -344,7 +349,9 @@ function cmdInit(args) {
|
|
|
344
349
|
.filter(Boolean)) {
|
|
345
350
|
out("");
|
|
346
351
|
out(`installing package ${pkg} from the registry`);
|
|
347
|
-
|
|
352
|
+
const installArgs = ["install", pkg, "--yes"];
|
|
353
|
+
if (args.without) installArgs.push("--without", String(args.without));
|
|
354
|
+
if (kernel(target, installArgs) !== 0) fail(`package ${pkg} failed to install (earlier packages stay installed)`);
|
|
348
355
|
installed++;
|
|
349
356
|
if (reportPending(target)) fail(`framework and ${pkg} are installed, but the workspace is not converged until the semantic migration above is cleared`);
|
|
350
357
|
}
|
|
@@ -506,6 +513,35 @@ notes:
|
|
|
506
513
|
`);
|
|
507
514
|
}
|
|
508
515
|
|
|
516
|
+
const COMMAND_HELP = {
|
|
517
|
+
init: "usage: owfa init [dir] [--name NAME] [--packages a,b] [--without m,n] [--force]\n\n" +
|
|
518
|
+
"deploy a fresh workspace (additive: existing files and AGENTS.md are preserved).\n" +
|
|
519
|
+
"python 3.9+ required for the framework; node 16.7+ runs this installer only.\n" +
|
|
520
|
+
"--packages installs registry packages; --without prunes members from them (dependency-closed subsets only).\n" +
|
|
521
|
+
"--force re-copies the framework trees over an existing instance (instance data is never touched).",
|
|
522
|
+
upgrade: "usage: owfa upgrade [dir] [--check] [--backup] [--force]\n\n" +
|
|
523
|
+
"replace the framework trees with this channel's version and converge.\n" +
|
|
524
|
+
"--check (alias --dry-run) is read-only; --backup snapshots the current trees to .owfa/backup/.\n" +
|
|
525
|
+
"instance data, installed packages, pending migrations and standalone skills are preserved.",
|
|
526
|
+
doctor: "usage: owfa doctor [dir]\n\n" +
|
|
527
|
+
"health check: python, framework version, AGENTS.md injection markers, kernel validate.\n" +
|
|
528
|
+
"exits non-zero when any required condition fails.",
|
|
529
|
+
add: "usage: owfa add <pkg> [--yes] [--without m,n] (run inside the instance)\n\n" +
|
|
530
|
+
"install a domain package from the registry, then converge.\n" +
|
|
531
|
+
"--without prunes members (dependency-closed subsets only); unindexed repos need --yes.",
|
|
532
|
+
list: "usage: owfa list\n\nregistry catalog: versions, compat floors, installed marks.",
|
|
533
|
+
pkg: "usage: owfa pkg\n\ninstalled package inventory: members, connectors, core remainder.",
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
const COMMAND_FLAGS = {
|
|
537
|
+
init: ["force", "help", "h", ...FLAG_VALUE],
|
|
538
|
+
upgrade: ["check", "dry-run", "backup", "force", "help", "h"],
|
|
539
|
+
doctor: ["help", "h"],
|
|
540
|
+
add: ["yes", "without", "path", "from", "help", "h", ...[]],
|
|
541
|
+
list: ["help", "h"],
|
|
542
|
+
pkg: ["help", "h"],
|
|
543
|
+
};
|
|
544
|
+
|
|
509
545
|
function main() {
|
|
510
546
|
const argv = process.argv.slice(2);
|
|
511
547
|
const cmd = argv[0];
|
|
@@ -517,7 +553,25 @@ function main() {
|
|
|
517
553
|
out(`owfa ${PKG.version} (framework ${FRAMEWORK_VERSION})`);
|
|
518
554
|
return 0;
|
|
519
555
|
}
|
|
556
|
+
if (!(cmd in COMMAND_HELP)) {
|
|
557
|
+
out(`unknown command: ${cmd}`);
|
|
558
|
+
out("");
|
|
559
|
+
usage();
|
|
560
|
+
return 2;
|
|
561
|
+
}
|
|
562
|
+
if (argv.slice(1).includes("--help") || argv.slice(1).includes("-h")) {
|
|
563
|
+
out(`owfa ${PKG.version}`);
|
|
564
|
+
out(COMMAND_HELP[cmd]);
|
|
565
|
+
return 0;
|
|
566
|
+
}
|
|
520
567
|
const args = parseArgs(argv.slice(1));
|
|
568
|
+
const known = new Set(COMMAND_FLAGS[cmd]);
|
|
569
|
+
const bad = [...args.used].filter((k) => !known.has(k));
|
|
570
|
+
if (bad.length) {
|
|
571
|
+
out(`unknown option${bad.length > 1 ? "s" : ""} for ${cmd}: ${bad.map((b) => "--" + b.replace(/^-+/, "")).join(", ")}`);
|
|
572
|
+
out(`see: owfa ${cmd} --help`);
|
|
573
|
+
return 2;
|
|
574
|
+
}
|
|
521
575
|
switch (cmd) {
|
|
522
576
|
case "init":
|
|
523
577
|
return cmdInit(args);
|