owfadev 0.3.1 → 0.4.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/.agents/skills/owfa-check/SKILL.md +49 -0
- package/.agents/skills/{docs → owfa-docs}/SKILL.md +5 -5
- package/.agents/skills/{wiki_plugin_kernel → owfa-kernel}/SKILL.md +13 -12
- package/{.meta/command/plugin → .agents/skills/owfa-plugin}/SKILL.md +10 -10
- package/.agents/skills/{query → owfa-query}/SKILL.md +5 -6
- package/.owfa/VERSION +1 -0
- package/.owfa/command/owfa-check/SKILL.md +49 -0
- package/{.meta/command/docs → .owfa/command/owfa-docs}/SKILL.md +5 -5
- package/{.meta/command/wiki_plugin_kernel → .owfa/command/owfa-kernel}/SKILL.md +13 -12
- package/{.agents/skills/plugin → .owfa/command/owfa-plugin}/SKILL.md +10 -10
- package/{.meta/command/query → .owfa/command/owfa-query}/SKILL.md +5 -6
- package/{.meta → .owfa}/docs/agent-behavior.md +3 -2
- package/{.meta → .owfa}/docs/agents-example.md +1 -1
- package/{.meta → .owfa}/docs/conversion.md +2 -2
- package/{.meta → .owfa}/docs/deployment.md +51 -46
- package/{.meta → .owfa}/docs/developer.md +4 -4
- package/{.meta → .owfa}/docs/distribution.md +21 -19
- package/.owfa/docs/home.md +61 -0
- package/{.meta → .owfa}/docs/intro.md +5 -5
- package/{.meta → .owfa}/docs/log-example.md +1 -1
- package/{.meta → .owfa}/docs/map.md +4 -3
- package/{.meta → .owfa}/docs/mechanics.md +11 -11
- package/{.meta → .owfa}/docs/naming.md +7 -7
- package/{.meta → .owfa}/docs/packages.md +80 -15
- package/.owfa/docs/plugin-lifecycle.md +70 -0
- package/{.meta → .owfa}/docs/quickstart.md +12 -12
- package/{.meta → .owfa}/docs/research-user-profile.md +1 -1
- package/{.meta → .owfa}/docs/sandbox.md +3 -3
- package/{.meta → .owfa}/docs/usage.md +1 -1
- package/{.meta → .owfa}/protocol/actions.md +6 -6
- package/.owfa/protocol/registry.yaml +34 -0
- package/.owfa/scripts/owfa_home.py +273 -0
- package/{.meta/scripts/wiki_plugin_kernel.py → .owfa/scripts/owfa_kernel.py} +521 -77
- package/{.meta → .owfa}/scripts/pipeline.py +9 -9
- package/.owfa/scripts/wiki_plugin_kernel.py +15 -0
- package/{.meta → .owfa}/scripts/wikilib.py +2 -2
- package/.owfa/tests/test_capabilities.py +311 -0
- package/{.meta → .owfa}/tests/test_install_preflight.py +11 -7
- package/.owfa/tests/test_owfa_home.py +127 -0
- package/.owfa/tests/test_version_scheme.py +63 -0
- package/README.md +32 -24
- package/README.zh-CN.md +32 -24
- package/bin/owfa.js +525 -65
- package/package.json +3 -2
- package/payloads/personal/PKG.yaml +15 -0
- package/payloads/personal/README.md +7 -0
- package/{.meta/plugins/core → payloads/personal}/calendar/PLUGIN.md +1 -0
- package/{.meta/plugins/core → payloads/personal}/calendar/PLUGIN.yaml +3 -3
- package/{.agents/skills/map → payloads/personal/command/owfa-map}/SKILL.md +9 -9
- package/{.meta/command/profile → payloads/personal/command/owfa-profile}/SKILL.md +11 -11
- package/{.agents/skills/save → payloads/personal/command/owfa-save}/SKILL.md +10 -10
- package/{.meta/plugins/core → payloads/personal}/cron/PLUGIN.md +2 -0
- package/{.meta/plugins/core → payloads/personal}/cron/PLUGIN.yaml +3 -3
- package/{.meta/plugins/core → payloads/personal}/device/PLUGIN.md +1 -0
- package/{.meta/plugins/core → payloads/personal}/device/PLUGIN.yaml +3 -3
- package/{.meta/plugins/core → payloads/personal}/domain/PLUGIN.md +2 -0
- package/payloads/personal/domain/PLUGIN.yaml +8 -0
- package/{.meta/plugins/core → payloads/personal}/hot/PLUGIN.yaml +1 -1
- package/{.meta/plugins/core → payloads/personal}/index/PLUGIN.md +1 -0
- package/{.meta/plugins/core → payloads/personal}/index/PLUGIN.yaml +4 -4
- package/{.meta/plugins/core → payloads/personal}/instance-git/PLUGIN.md +5 -0
- package/{.meta/plugins/core → payloads/personal}/instance-git/PLUGIN.yaml +3 -3
- package/{.meta/plugins/core → payloads/personal}/language/PLUGIN.md +1 -0
- package/{.meta/plugins/core → payloads/personal}/language/PLUGIN.yaml +4 -4
- package/{.meta/plugins/core → payloads/personal}/link/PLUGIN.md +1 -0
- package/{.meta/plugins/core → payloads/personal}/link/PLUGIN.yaml +3 -3
- package/{.meta/plugins/core → payloads/personal}/link/scripts/check.py +1 -1
- package/{.meta/plugins/core → payloads/personal}/log/PLUGIN.md +1 -0
- package/{.meta/plugins/core → payloads/personal}/log/PLUGIN.yaml +4 -4
- package/{.meta/plugins/core → payloads/personal}/mapping/PLUGIN.yaml +1 -1
- package/{.meta/plugins/core → payloads/personal}/notes/PLUGIN.md +1 -0
- package/{.meta/plugins/core → payloads/personal}/notes/PLUGIN.yaml +4 -4
- package/{.meta/plugins/core → payloads/personal}/sessions/PLUGIN.md +2 -1
- package/{.meta/plugins/core → payloads/personal}/sessions/PLUGIN.yaml +5 -5
- package/{.meta/plugins/core → payloads/personal}/structure/PLUGIN.md +2 -0
- package/{.meta/plugins/core → payloads/personal}/structure/PLUGIN.yaml +3 -3
- package/{.meta/plugins/core → payloads/personal}/structure/scripts/check.py +1 -1
- package/{.meta/plugins/core → payloads/personal}/tag/PLUGIN.md +2 -0
- package/{.meta/plugins/core → payloads/personal}/tag/PLUGIN.yaml +3 -3
- package/{.meta/plugins/core → payloads/personal}/tag/scripts/check.py +3 -1
- package/{.meta/plugins/core → payloads/personal}/tmp/PLUGIN.md +1 -0
- package/{.meta/plugins/core → payloads/personal}/tmp/PLUGIN.yaml +3 -3
- package/{.meta/plugins/core → payloads/personal}/todo/PLUGIN.md +2 -0
- package/{.meta/plugins/core → payloads/personal}/todo/PLUGIN.yaml +6 -5
- package/payloads/personal/todo/scripts/check.py +51 -0
- package/{.meta/plugins/core → payloads/personal}/trust/PLUGIN.md +1 -0
- package/{.meta/plugins/core → payloads/personal}/trust/PLUGIN.yaml +3 -3
- package/{.meta/plugins/core → payloads/personal}/user-profile/PLUGIN.md +2 -0
- package/{.meta/plugins/core → payloads/personal}/user-profile/PLUGIN.yaml +7 -6
- package/payloads/personal/user-profile/scripts/check.py +20 -0
- package/{.meta/plugins/core → payloads/personal}/vault/PLUGIN.md +1 -0
- package/payloads/personal/vault/PLUGIN.yaml +13 -0
- package/{.meta/plugins/core → payloads/personal}/wiki/PLUGIN.md +1 -0
- package/payloads/personal/wiki/PLUGIN.yaml +8 -0
- package/templates/AGENTS.template.md +2 -2
- package/.agents/skills/check/SKILL.md +0 -130
- package/.agents/skills/profile/SKILL.md +0 -75
- package/.meta/VERSION +0 -1
- package/.meta/command/check/SKILL.md +0 -130
- package/.meta/command/map/SKILL.md +0 -72
- package/.meta/command/save/SKILL.md +0 -94
- package/.meta/plugins/core/domain/PLUGIN.yaml +0 -8
- package/.meta/plugins/core/vault/PLUGIN.yaml +0 -13
- package/.meta/plugins/core/wiki/PLUGIN.yaml +0 -8
- package/.meta/protocol/registry.yaml +0 -75
- /package/{.meta → .owfa}/command/asset-read/SKILL.md +0 -0
- /package/{.meta → .owfa}/docs/research-landscape.md +0 -0
- /package/{.meta → .owfa}/protocol/experiments.md +0 -0
- /package/{.meta/plugins/core → payloads/personal}/device/scripts/check.py +0 -0
- /package/{.meta/plugins/core → payloads/personal}/hot/PLUGIN.md +0 -0
- /package/{.meta/plugins/core → payloads/personal}/instance-git/scripts/check.py +0 -0
- /package/{.meta/plugins/core → payloads/personal}/language/scripts/check.py +0 -0
- /package/{.meta/plugins/core → payloads/personal}/log/scripts/check.py +0 -0
- /package/{.meta/plugins/core → payloads/personal}/mapping/PLUGIN.md +0 -0
- /package/{.meta/plugins/core → payloads/personal}/mapping/scripts/check.py +0 -0
- /package/{.meta/plugins/core → payloads/personal}/sessions/scripts/check.py +0 -0
- /package/{.meta/plugins/core → payloads/personal}/trust/scripts/check.py +0 -0
package/bin/owfa.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* things the framework itself cannot own: fetching a complete framework tree,
|
|
7
7
|
* initializing a directory without hand-copying it, and replacing that tree
|
|
8
8
|
* on upgrade. Everything semantic stays in the Python kernel
|
|
9
|
-
* (.
|
|
9
|
+
* (.owfa/scripts/owfa_kernel.py) — this file only orchestrates.
|
|
10
10
|
*
|
|
11
11
|
* Design rules:
|
|
12
12
|
* - zero runtime dependencies; Node is needed to install, not to use
|
|
@@ -28,13 +28,24 @@ const PKG = JSON.parse(fs.readFileSync(path.join(CLI_ROOT, "package.json"), "utf
|
|
|
28
28
|
const FRAMEWORK_VERSION = readVersion(CLI_ROOT) || "?";
|
|
29
29
|
const INJECT_START = "<!-- wiki-inject:start -->";
|
|
30
30
|
const INJECT_END = "<!-- wiki-inject:end -->";
|
|
31
|
-
const PAYLOAD = [".
|
|
32
|
-
|
|
31
|
+
const PAYLOAD = [".owfa", ".agents"];
|
|
32
|
+
// v0.4 de-parasitization (final shape): the channel no longer hardcodes instance
|
|
33
|
+
// skeleton dirs and no longer ships .owfa/plugins/core/ — the 21 core plugins live
|
|
34
|
+
// in the owfa-personal suite repo, embedded at payloads/personal/, and that blessed
|
|
35
|
+
// copy is what a fresh init installs by default (offline, --path channel). Packages
|
|
36
|
+
// declare their own skeleton (`skeleton:` in PKG.yaml; the kernel mkdirs them on
|
|
37
|
+
// install), so the default two-stage init lands both framework and skeleton.
|
|
38
|
+
const BLESSED_SUITE = path.join(CLI_ROOT, "payloads", "personal");
|
|
39
|
+
// 0.3 instances carry the framework in .meta/; upgrade migrates them to .owfa/
|
|
40
|
+
// and leaves a .meta shim (symlink/junction) so old absolute paths keep working.
|
|
41
|
+
const LEGACY_META = ".meta";
|
|
42
|
+
const MIGRATE_SCRATCH = ".owfa-migrate"; // transient replace/ parking (was .owfa — the name now belongs to the framework tree)
|
|
43
|
+
const BACKUP_DIR = ".owfa-backup"; // durable opt-in upgrade snapshots (was .owfa/backup)
|
|
33
44
|
// Kernel state that belongs to the instance, not to the framework tree.
|
|
34
45
|
const PRESERVED = [
|
|
35
|
-
path.join(".
|
|
36
|
-
path.join(".
|
|
37
|
-
path.join(".
|
|
46
|
+
path.join(".owfa", "migrations", "pending.json"),
|
|
47
|
+
path.join(".owfa", "kernel-journal.log"),
|
|
48
|
+
path.join(".owfa", "registry-cache.yaml"),
|
|
38
49
|
];
|
|
39
50
|
|
|
40
51
|
// ---------------------------------------------------------------------------
|
|
@@ -42,11 +53,14 @@ const PRESERVED = [
|
|
|
42
53
|
// ---------------------------------------------------------------------------
|
|
43
54
|
|
|
44
55
|
function readVersion(root) {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
56
|
+
// ".meta" is the frozen 0.3 layout — a historical constant, safe to inline
|
|
57
|
+
// here (LEGACY_META is declared later and readVersion runs at module init).
|
|
58
|
+
for (const dir of [".owfa", ".meta"]) {
|
|
59
|
+
try {
|
|
60
|
+
return fs.readFileSync(path.join(root, dir, "VERSION"), "utf8").trim();
|
|
61
|
+
} catch {}
|
|
49
62
|
}
|
|
63
|
+
return null;
|
|
50
64
|
}
|
|
51
65
|
|
|
52
66
|
function out(line = "") {
|
|
@@ -62,7 +76,7 @@ function rel(root, p) {
|
|
|
62
76
|
return path.relative(root, p) || ".";
|
|
63
77
|
}
|
|
64
78
|
|
|
65
|
-
const FLAG_VALUE = ["name", "packages", "without", "from", "path"];
|
|
79
|
+
const FLAG_VALUE = ["name", "packages", "without", "from", "path", "core"];
|
|
66
80
|
|
|
67
81
|
function parseArgs(argv) {
|
|
68
82
|
const args = { _: [], flags: new Set(), used: new Set() };
|
|
@@ -92,6 +106,35 @@ function parseArgs(argv) {
|
|
|
92
106
|
return args;
|
|
93
107
|
}
|
|
94
108
|
|
|
109
|
+
// 0.3 quirk (review P1 round 3): the 0.3 channel wrote `upgrade --backup`
|
|
110
|
+
// snapshots to .owfa/backup/ even though its framework lived in .meta/. On
|
|
111
|
+
// migration that stray .owfa made replacePayloadTree park-and-delete the
|
|
112
|
+
// user's only pre-upgrade snapshots AND left .meta unparked (legacySwap never
|
|
113
|
+
// fired: no shim, no journal carry). Rescue the snapshots into the 0.4 backup
|
|
114
|
+
// home first; an emptied leftover .owfa is removed so the legacy swap takes
|
|
115
|
+
// over. A real 0.4 framework tree (.owfa/VERSION present) is never touched.
|
|
116
|
+
function rescueLegacyBackup(target) {
|
|
117
|
+
const owfa = path.join(target, ".owfa");
|
|
118
|
+
const meta = path.join(target, LEGACY_META);
|
|
119
|
+
if (!fs.existsSync(owfa) || fs.existsSync(path.join(owfa, "VERSION"))) return;
|
|
120
|
+
if (!fs.existsSync(path.join(meta, "VERSION"))) return; // no legacy framework — not the 0.3 shape
|
|
121
|
+
const stray = path.join(owfa, "backup");
|
|
122
|
+
if (fs.existsSync(stray)) {
|
|
123
|
+
const home = path.join(target, BACKUP_DIR);
|
|
124
|
+
fs.mkdirSync(home, { recursive: true });
|
|
125
|
+
for (const d of fs.readdirSync(stray)) {
|
|
126
|
+
if (fs.existsSync(path.join(home, d))) {
|
|
127
|
+
out(`warning: backup snapshot ${d} left in .owfa/backup/ — ${BACKUP_DIR}/${d} already exists`);
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
fs.renameSync(path.join(stray, d), path.join(home, d));
|
|
131
|
+
}
|
|
132
|
+
fs.rmSync(stray, { recursive: true, force: true });
|
|
133
|
+
out(`rescued: 0.3-era upgrade snapshots moved to ${BACKUP_DIR}/`);
|
|
134
|
+
}
|
|
135
|
+
if (fs.readdirSync(owfa).length === 0) fs.rmSync(owfa, { recursive: true });
|
|
136
|
+
}
|
|
137
|
+
|
|
95
138
|
function copyPayload(target, { replace = false } = {}) {
|
|
96
139
|
const skip = (src) => {
|
|
97
140
|
const base = path.basename(src);
|
|
@@ -104,7 +147,12 @@ function copyPayload(target, { replace = false } = {}) {
|
|
|
104
147
|
const dst = path.join(target, entry);
|
|
105
148
|
if (!fs.existsSync(src)) fail(`package is missing ${entry}/ — the npm tarball is incomplete`);
|
|
106
149
|
if (replace) {
|
|
107
|
-
|
|
150
|
+
// 0.3 → 0.4 migration: the incoming .owfa/ has no counterpart on disk yet,
|
|
151
|
+
// but the legacy .meta/ tree must go through the same park-and-carry swap.
|
|
152
|
+
if (entry === ".owfa") rescueLegacyBackup(target);
|
|
153
|
+
const legacySwap =
|
|
154
|
+
entry === ".owfa" && !fs.existsSync(dst) && fs.existsSync(path.join(target, LEGACY_META));
|
|
155
|
+
if (fs.existsSync(dst) || legacySwap) replacePayloadTree(target, entry, skip);
|
|
108
156
|
} else {
|
|
109
157
|
fs.mkdirSync(path.dirname(dst), { recursive: true });
|
|
110
158
|
fs.cpSync(src, dst, { recursive: true, force: true, filter: skip });
|
|
@@ -116,19 +164,36 @@ function copyPayload(target, { replace = false } = {}) {
|
|
|
116
164
|
// files (review P1, PR #53): installed package masters (kernel install), migration
|
|
117
165
|
// state (ledger / pending / dormant teardown), and skill copies the incoming tree
|
|
118
166
|
// does not ship (package connector skills, user standalone skills). Everything else
|
|
119
|
-
// under .
|
|
167
|
+
// under .owfa/ and .agents/ is framework-owned and replaced by the channel.
|
|
120
168
|
const META_MERGE_DIRS = [path.join("plugins", "domains"), "migrations"];
|
|
121
169
|
|
|
122
|
-
// .
|
|
170
|
+
// v0.4 renames the framework command/skill masters to owfa-* (review P1, PR #57
|
|
171
|
+
// round 2): a real 0.3 tree carries the retired names, and the difference-based
|
|
172
|
+
// carry-over would otherwise treat them as package-owned state and resurrect
|
|
173
|
+
// them beside their successors — 8 duplicate masters, `kernel all` blocked by
|
|
174
|
+
// one-sided owner-mapping errors. Retired framework names never carry; the map
|
|
175
|
+
// documents where each one lives now.
|
|
176
|
+
const RETIRED_MASTERS = new Map([
|
|
177
|
+
["check", "owfa-check"],
|
|
178
|
+
["docs", "owfa-docs"],
|
|
179
|
+
["map", "owfa-map"],
|
|
180
|
+
["plugin", "owfa-plugin"],
|
|
181
|
+
["profile", "owfa-profile"],
|
|
182
|
+
["query", "owfa-query"],
|
|
183
|
+
["save", "owfa-save"],
|
|
184
|
+
["wiki_plugin_kernel", "owfa-kernel"],
|
|
185
|
+
]);
|
|
186
|
+
|
|
187
|
+
// .owfa/command/ is mixed-ownership: framework commands ship in the fresh tree,
|
|
123
188
|
// package commands (kernel install) and anything the channel no longer ships are
|
|
124
189
|
// carried over by difference — the fresh copy wins for shared names.
|
|
125
190
|
function carryOverCommands(target, metaAttic) {
|
|
126
191
|
const oldDir = path.join(metaAttic, "command");
|
|
127
192
|
if (!fs.existsSync(oldDir)) return;
|
|
128
|
-
const freshDir = path.join(target, ".
|
|
193
|
+
const freshDir = path.join(target, ".owfa", "command");
|
|
129
194
|
const fresh = new Set(fs.existsSync(freshDir) ? fs.readdirSync(freshDir) : []);
|
|
130
195
|
for (const d of fs.readdirSync(oldDir)) {
|
|
131
|
-
if (fresh.has(d)) continue;
|
|
196
|
+
if (fresh.has(d) || RETIRED_MASTERS.has(d)) continue;
|
|
132
197
|
fs.cpSync(path.join(oldDir, d), path.join(freshDir, d), { recursive: true, force: true });
|
|
133
198
|
}
|
|
134
199
|
}
|
|
@@ -139,11 +204,11 @@ function mergeTree(src, dst) {
|
|
|
139
204
|
}
|
|
140
205
|
|
|
141
206
|
function carryOverEntry(target, entry, attic) {
|
|
142
|
-
if (entry === ".
|
|
143
|
-
for (const d of META_MERGE_DIRS) mergeTree(path.join(attic, d), path.join(target, ".
|
|
207
|
+
if (entry === ".owfa") {
|
|
208
|
+
for (const d of META_MERGE_DIRS) mergeTree(path.join(attic, d), path.join(target, ".owfa", d));
|
|
144
209
|
carryOverCommands(target, attic);
|
|
145
210
|
for (const p of PRESERVED) {
|
|
146
|
-
const s = path.join(attic, path.relative(".
|
|
211
|
+
const s = path.join(attic, path.relative(".owfa", p)); // PRESERVED is target-relative; attic IS the old framework tree
|
|
147
212
|
if (fs.existsSync(s)) {
|
|
148
213
|
const d = path.join(target, p);
|
|
149
214
|
fs.mkdirSync(path.dirname(d), { recursive: true });
|
|
@@ -156,48 +221,296 @@ function carryOverEntry(target, entry, attic) {
|
|
|
156
221
|
const freshDir = path.join(target, ".agents", "skills");
|
|
157
222
|
const fresh = new Set(fs.existsSync(freshDir) ? fs.readdirSync(freshDir) : []);
|
|
158
223
|
for (const d of fs.readdirSync(oldSkills)) {
|
|
159
|
-
if (fresh.has(d)) continue; // framework-shipped
|
|
224
|
+
if (fresh.has(d) || RETIRED_MASTERS.has(d)) continue; // framework-shipped / retired-via-rename — the fresh copy wins
|
|
160
225
|
fs.cpSync(path.join(oldSkills, d), path.join(freshDir, d), { recursive: true, force: true });
|
|
161
226
|
}
|
|
162
227
|
}
|
|
163
228
|
}
|
|
164
229
|
|
|
165
|
-
// Review round 2 (PR #53): the parked tree under .owfa/replace/ holds the only
|
|
230
|
+
// Review round 2 (PR #53): the parked tree under .owfa-migrate/replace/ holds the only
|
|
166
231
|
// copy of the instance state between set-aside and carry-over. An interrupted
|
|
167
232
|
// swap (Windows: rename of a tree with an open file) must therefore never be
|
|
168
233
|
// implicitly cleaned by a retry, and a failure must roll the parked tree back.
|
|
169
234
|
function replacePayloadTree(target, entry, skip) {
|
|
170
235
|
const dst = path.join(target, entry);
|
|
171
|
-
|
|
236
|
+
// 0.3 → 0.4 layout migration: an instance whose framework still lives in
|
|
237
|
+
// .meta/ parks that tree the same way, so crash semantics and carry-over
|
|
238
|
+
// are identical to a normal same-layout swap.
|
|
239
|
+
const parkedFrom =
|
|
240
|
+
fs.existsSync(dst)
|
|
241
|
+
? dst
|
|
242
|
+
: entry === ".owfa" && fs.existsSync(path.join(target, LEGACY_META))
|
|
243
|
+
? path.join(target, LEGACY_META)
|
|
244
|
+
: null;
|
|
245
|
+
const attic = path.join(target, MIGRATE_SCRATCH, "replace", path.basename(entry).replace(/^\./, ""));
|
|
172
246
|
if (fs.existsSync(attic)) {
|
|
173
247
|
fail(
|
|
174
248
|
`interrupted replace detected: instance state is parked at ${attic} — ` +
|
|
175
249
|
`do NOT simply rerun. Restore it manually (delete the fresh ${entry}/, ` +
|
|
176
|
-
`rename the parked tree back to ${entry}/), then rerun.`
|
|
250
|
+
`rename the parked tree back to ${parkedFrom || entry}/), then rerun.`
|
|
177
251
|
);
|
|
178
252
|
}
|
|
253
|
+
if (!parkedFrom) return; // fresh init into a tree that never existed — nothing to replace
|
|
179
254
|
fs.mkdirSync(path.dirname(attic), { recursive: true });
|
|
180
255
|
try {
|
|
181
|
-
fs.renameSync(
|
|
256
|
+
fs.renameSync(parkedFrom, attic); // same volume — atomic enough for a directory swap
|
|
182
257
|
fs.cpSync(path.join(CLI_ROOT, entry), dst, { recursive: true, force: true, filter: skip });
|
|
183
258
|
carryOverEntry(target, entry, attic);
|
|
259
|
+
if (entry === ".owfa") rescueCoreForConversion(target, attic, dst);
|
|
184
260
|
} catch (e) {
|
|
185
261
|
try {
|
|
186
262
|
fs.rmSync(dst, { recursive: true, force: true }); // partial fresh copy
|
|
187
|
-
fs.renameSync(attic,
|
|
188
|
-
fail(`replace failed (${e.message}) — rolled back: ${
|
|
263
|
+
fs.renameSync(attic, parkedFrom); // parked tree back in place
|
|
264
|
+
fail(`replace failed (${e.message}) — rolled back: ${parkedFrom} restored from the parked tree; fix the underlying error before rerunning`);
|
|
189
265
|
} catch (e2) {
|
|
190
266
|
fail(
|
|
191
267
|
`replace failed (${e.message}) AND rollback failed (${e2.message}) — the previous tree ` +
|
|
192
|
-
`is parked at ${attic}; do NOT rerun until it is restored to ${
|
|
268
|
+
`is parked at ${attic}; do NOT rerun until it is restored to ${parkedFrom}`
|
|
193
269
|
);
|
|
194
270
|
}
|
|
195
271
|
}
|
|
196
|
-
fs.rmSync(path.join(target,
|
|
272
|
+
fs.rmSync(path.join(target, MIGRATE_SCRATCH, "replace"), { recursive: true, force: true });
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// v0.4 core de-parasitization, rescue half: when the parked tree carries
|
|
276
|
+
// .owfa/plugins/core/ but the incoming channel tree does not (core stopped
|
|
277
|
+
// shipping), the parked masters are the only copy — move them out of the attic
|
|
278
|
+
// before it is deleted. The conversion itself (below) runs once the whole
|
|
279
|
+
// payload replace has finished, so it converges against the complete fresh tree.
|
|
280
|
+
// Inside the replace try-block: a failure here rolls the whole swap back.
|
|
281
|
+
function rescueCoreForConversion(target, attic, freshOwfa) {
|
|
282
|
+
const parkedCore = path.join(attic, "plugins", "core");
|
|
283
|
+
if (!fs.existsSync(parkedCore)) return;
|
|
284
|
+
if (fs.existsSync(path.join(freshOwfa, "plugins", "core"))) return; // compat: channel still ships core — nothing to convert
|
|
285
|
+
const hold = path.join(target, MIGRATE_SCRATCH, "convert", "core");
|
|
286
|
+
if (fs.existsSync(hold)) return; // a previous failed attempt kept the rescued masters — the convert step will rerun
|
|
287
|
+
fs.mkdirSync(path.dirname(hold), { recursive: true });
|
|
288
|
+
fs.renameSync(parkedCore, hold);
|
|
289
|
+
out("rescued: .owfa/plugins/core/ parked at " + rel(target, hold) + " — converting it into the personal suite package below");
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// v0.4 core de-parasitization, convert half: mechanically adopt the rescued
|
|
293
|
+
// core masters as the `personal` suite package (a fact-accomplished conversion —
|
|
294
|
+
// the plugins are already in daily use in this instance, the package shape is
|
|
295
|
+
// new). Stages a synthetic package under .owfa-migrate/convert/personal/ (NOT
|
|
296
|
+
// directly under domains/ — `kernel install --path` owns that landing) and runs
|
|
297
|
+
// `kernel install --path ... --yes` for sweep symmetry + projection convergence
|
|
298
|
+
// (--yes is legitimate here: the channel itself is converting its own former
|
|
299
|
+
// payload, a mechanical adoption, not a trust decision).
|
|
300
|
+
//
|
|
301
|
+
// Family closure (kernel D5 invariant): every member must reach the family root
|
|
302
|
+
// through depends. The rescued set's own dependency roots (members whose depends
|
|
303
|
+
// has no edge into the rescued set — today: domain, instance-git, wiki) get a
|
|
304
|
+
// synthetic `- personal` edge in the STAGING copies only, so the adopted roster
|
|
305
|
+
// validates. When the real owfa-personal suite later replaces this package via
|
|
306
|
+
// `kernel upgrade`, the canonical manifests take over. The conversion stages no
|
|
307
|
+
// semantic migration (no migrations/ dir: nothing semantic changes — plugin
|
|
308
|
+
// bodies and projections are byte-identical), so there is nothing to verify-clear.
|
|
309
|
+
function convertCoreToPersonal(target) {
|
|
310
|
+
const hold = path.join(target, MIGRATE_SCRATCH, "convert", "core");
|
|
311
|
+
if (!fs.existsSync(hold)) return;
|
|
312
|
+
const pkgDir = path.join(target, ".owfa", "plugins", "domains", "personal");
|
|
313
|
+
if (fs.existsSync(pkgDir)) {
|
|
314
|
+
out("convert: domains/personal already present — rescued core treated as already converted, holding tree discarded");
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
const members = fs
|
|
318
|
+
.readdirSync(hold)
|
|
319
|
+
.filter((d) => fs.existsSync(path.join(hold, d, "PLUGIN.yaml")))
|
|
320
|
+
.sort();
|
|
321
|
+
if (!members.length) fail("convert: rescued .owfa/plugins/core/ carries no plugin masters — nothing to convert");
|
|
322
|
+
const staging = path.join(target, MIGRATE_SCRATCH, "convert", "personal");
|
|
323
|
+
fs.mkdirSync(staging, { recursive: true });
|
|
324
|
+
for (const m of members) fs.cpSync(path.join(hold, m), path.join(staging, m), { recursive: true });
|
|
325
|
+
|
|
326
|
+
// 0.3-era rescued manifests still declare the pre-rename command ids (map /
|
|
327
|
+
// profile / save) while the command masters riding with this conversion carry
|
|
328
|
+
// the v0.4 successors — rewrite the staging copies' `commands:` lists so the
|
|
329
|
+
// adopted roster binds (flow list and block list both handled).
|
|
330
|
+
for (const m of members) {
|
|
331
|
+
const mf = path.join(staging, m, "PLUGIN.yaml");
|
|
332
|
+
const lines = fs.readFileSync(mf, "utf8").split("\n");
|
|
333
|
+
let inCommands = false;
|
|
334
|
+
for (let i = 0; i < lines.length; i++) {
|
|
335
|
+
const flow = lines[i].match(/^((?:commands|usage_routes):\s*)\[(.*)\](\s*)$/);
|
|
336
|
+
if (flow) {
|
|
337
|
+
const items = flow[2].split(",").map((s) => s.trim()).filter(Boolean)
|
|
338
|
+
.map((s) => RETIRED_MASTERS.get(s) || s);
|
|
339
|
+
lines[i] = `${flow[1]}[${items.join(", ")}]${flow[3]}`;
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
if (/^(commands|usage_routes):\s*$/.test(lines[i])) {
|
|
343
|
+
inCommands = true;
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
if (inCommands) {
|
|
347
|
+
const item = lines[i].match(/^(\s*-\s*)(\S+)\s*$/);
|
|
348
|
+
if (!item) {
|
|
349
|
+
inCommands = false;
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
lines[i] = item[1] + (RETIRED_MASTERS.get(item[2]) || item[2]);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
fs.writeFileSync(mf, lines.join("\n"), "utf8");
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// the suite's command masters ride along: the members adopted above declare
|
|
359
|
+
// them (mapping/notes/sessions/user-profile `commands:`), and the embedded
|
|
360
|
+
// blessed copy carries the canonical copies — without them the converted
|
|
361
|
+
// instance would lose map/save/profile until a manual `kernel upgrade personal`
|
|
362
|
+
const blessedCmd = path.join(CLI_ROOT, "payloads", "personal", "command");
|
|
363
|
+
if (fs.existsSync(blessedCmd)) {
|
|
364
|
+
const stagingCmd = path.join(staging, "command");
|
|
365
|
+
fs.mkdirSync(stagingCmd, { recursive: true });
|
|
366
|
+
for (const d of fs.readdirSync(blessedCmd)) {
|
|
367
|
+
fs.cpSync(path.join(blessedCmd, d), path.join(stagingCmd, d), { recursive: true });
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// family-root edge: dependency roots of the rescued set adopt `personal`
|
|
372
|
+
const memberSet = new Set(members);
|
|
373
|
+
for (const m of members) {
|
|
374
|
+
const mf = path.join(staging, m, "PLUGIN.yaml");
|
|
375
|
+
let text = fs.readFileSync(mf, "utf8");
|
|
376
|
+
const depMatch = text.match(/^depends:\s*(.*)$/m);
|
|
377
|
+
const deps = depMatch
|
|
378
|
+
? depMatch[1].replace(/^\[|\]$/g, "").split(",").map((s) => s.trim().replace(/^"|"$/g, "")).filter(Boolean)
|
|
379
|
+
: [];
|
|
380
|
+
if (deps.some((d) => memberSet.has(d) || d === "personal")) continue; // reaches the family through the set already
|
|
381
|
+
if (depMatch && depMatch[1].trim() === "[]") {
|
|
382
|
+
text = text.replace(/^depends:\s*\[\]\s*$/m, "depends:\n - personal");
|
|
383
|
+
} else if (depMatch) {
|
|
384
|
+
text = text.replace(/^depends:.*$/m, (line) => line + "\n - personal");
|
|
385
|
+
} else {
|
|
386
|
+
text = text.replace(/^(id:.*)$/m, "$1\ndepends:\n - personal");
|
|
387
|
+
}
|
|
388
|
+
fs.writeFileSync(mf, text, "utf8");
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// synthesize the family root plugin
|
|
392
|
+
const root = path.join(staging, "personal");
|
|
393
|
+
fs.mkdirSync(root, { recursive: true });
|
|
394
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
395
|
+
fs.writeFileSync(
|
|
396
|
+
path.join(root, "PLUGIN.yaml"),
|
|
397
|
+
[
|
|
398
|
+
`# synthesized by the owfa channel: v0.4 core de-parasitization conversion (upgrade)`,
|
|
399
|
+
`# adopts the framework-shipped core masters as a package; replaced by the`,
|
|
400
|
+
`# canonical owfa-personal suite on the next \`kernel upgrade personal\`.`,
|
|
401
|
+
`id: personal`,
|
|
402
|
+
`schema_version: 1`,
|
|
403
|
+
`version: 0.1`,
|
|
404
|
+
`depends: []`,
|
|
405
|
+
`updated: ${today}`,
|
|
406
|
+
`attachment: []`,
|
|
407
|
+
`fields: {}`,
|
|
408
|
+
`inject: ${JSON.stringify("personal suite (converted from framework core by the 0.4 channel upgrade) — family identity anchor; members, skeleton and teardown anchors are declared in PKG.yaml; canonical source: owfadev/owfa-personal")}`,
|
|
409
|
+
``,
|
|
410
|
+
].join("\n"),
|
|
411
|
+
"utf8"
|
|
412
|
+
);
|
|
413
|
+
fs.writeFileSync(path.join(root, "PLUGIN.md"), "# personal\n\nSynthesized family root for the converted core suite (0.4 channel upgrade).\nSee PKG.yaml for members, skeleton and teardown anchors.\n", "utf8");
|
|
414
|
+
|
|
415
|
+
// synthesize PKG.yaml — skeleton and anchors preserve the pre-0.4 channel
|
|
416
|
+
// contract mechanically (INSTANCE_DIRS and the kernel's former teardown
|
|
417
|
+
// anchor surface), so the adopted instance behaves exactly as before
|
|
418
|
+
const anchors = ["wiki/profile.md", "wiki/language.md", "wiki/calendar.md", "wiki/todo.md", "wiki/cron/"];
|
|
419
|
+
const lines = [
|
|
420
|
+
`# synthesized by the owfa channel: v0.4 core de-parasitization conversion (upgrade)`,
|
|
421
|
+
`id: personal`,
|
|
422
|
+
// kind: suite — the conversion product is a rootless-suite package (the
|
|
423
|
+
// owfa-personal shape): the synthesized root below is a family-identity
|
|
424
|
+
// anchor only, the closure law does not apply, and the canonical rootless
|
|
425
|
+
// suite can replace this package via `kernel upgrade personal` cleanly
|
|
426
|
+
`kind: suite`,
|
|
427
|
+
`version: 0.1.0`,
|
|
428
|
+
`compat: ">= 0.4"`,
|
|
429
|
+
`members:`,
|
|
430
|
+
` - personal`,
|
|
431
|
+
...members.map((m) => ` - ${m}`),
|
|
432
|
+
`skeleton: [vault/, wiki/notes/, wiki/sessions/, wiki/vault/]`,
|
|
433
|
+
`uninstall:`,
|
|
434
|
+
` anchors: [${anchors.join(", ")}]`,
|
|
435
|
+
``,
|
|
436
|
+
];
|
|
437
|
+
fs.writeFileSync(path.join(staging, "PKG.yaml"), lines.join("\n"), "utf8");
|
|
438
|
+
|
|
439
|
+
out("");
|
|
440
|
+
out(`convert: core suite (${members.length} members) -> package personal (kernel install --path, --yes: mechanical adoption)`);
|
|
441
|
+
if (kernel(target, ["install", "personal", "--path", staging, "--yes"]) !== 0) {
|
|
442
|
+
fail(
|
|
443
|
+
`core -> personal conversion failed — the rescued masters are preserved at ${rel(target, hold)}; ` +
|
|
444
|
+
`fix the error above and rerun \`owfa upgrade\` (the conversion resumes from the hold)`
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
// success: drop the conversion scratch (and the migrate root if now empty)
|
|
448
|
+
fs.rmSync(path.join(target, MIGRATE_SCRATCH, "convert"), { recursive: true, force: true });
|
|
449
|
+
const scratchRoot = path.join(target, MIGRATE_SCRATCH);
|
|
450
|
+
if (fs.existsSync(scratchRoot) && fs.readdirSync(scratchRoot).length === 0) fs.rmdirSync(scratchRoot);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
// The 0.4 layout shim: 0.3-era absolute paths (.meta/...) keep working through a
|
|
455
|
+
// directory shim. POSIX symlink; Windows junction (no admin rights needed).
|
|
456
|
+
// Idempotent and self-healing (review P1/P2 on this PR):
|
|
457
|
+
// - written on fresh init too — released 0.3 packages (cuhksz/lark/github) still
|
|
458
|
+
// instruct .meta/ paths, so EVERY instance shape gets the shim until 0.5;
|
|
459
|
+
// - on upgrade a stale shim (instance dir renamed or copied — Windows junctions
|
|
460
|
+
// are absolute) is detected via realpath and re-created;
|
|
461
|
+
// - a real .meta directory (0.3 layout that never migrated) is never touched.
|
|
462
|
+
function ensureMetaShim(target) {
|
|
463
|
+
const metaPath = path.join(target, LEGACY_META);
|
|
464
|
+
const owfa = path.join(target, ".owfa");
|
|
465
|
+
if (!fs.existsSync(owfa)) return "absent";
|
|
466
|
+
let kind = "missing";
|
|
467
|
+
// lstat, not existsSync: a dangling shim (instance dir moved — review P2 round 2)
|
|
468
|
+
// makes existsSync false, which would skip the stale branch and hit EEXIST below.
|
|
469
|
+
let mst = null;
|
|
470
|
+
try {
|
|
471
|
+
mst = fs.lstatSync(metaPath);
|
|
472
|
+
} catch {
|
|
473
|
+
mst = null; // genuinely absent
|
|
474
|
+
}
|
|
475
|
+
if (mst) {
|
|
476
|
+
if (!mst.isSymbolicLink()) return "real-dir";
|
|
477
|
+
kind = "link";
|
|
478
|
+
let stale = true;
|
|
479
|
+
try {
|
|
480
|
+
stale = fs.realpathSync(metaPath) !== fs.realpathSync(owfa);
|
|
481
|
+
} catch {
|
|
482
|
+
stale = true; // dangling link
|
|
483
|
+
}
|
|
484
|
+
if (!stale) {
|
|
485
|
+
ensureGitignore(target, LEGACY_META);
|
|
486
|
+
return "ok";
|
|
487
|
+
}
|
|
488
|
+
fs.rmSync(metaPath, { force: true }); // symlink/junction only — never a real dir
|
|
489
|
+
}
|
|
490
|
+
try {
|
|
491
|
+
fs.symlinkSync(".owfa", metaPath, process.platform === "win32" ? "junction" : "dir");
|
|
492
|
+
} catch (e) {
|
|
493
|
+
out(`warning: could not create the .meta shim (${e.message}) — 0.3-era scripts calling .meta/... will not resolve`);
|
|
494
|
+
return "failed";
|
|
495
|
+
}
|
|
496
|
+
ensureGitignore(target, LEGACY_META);
|
|
497
|
+
return kind === "link" ? "recreated" : "created";
|
|
197
498
|
}
|
|
198
499
|
|
|
199
|
-
|
|
200
|
-
|
|
500
|
+
// Windows: core.symlinks=false makes git traverse the junction as a plain
|
|
501
|
+
// directory, so without this line the framework is tracked twice (.owfa/ plus
|
|
502
|
+
// every .meta/* entry) and the 0.5 shim retirement reads as a mass deletion.
|
|
503
|
+
// Instance-git contract: adding an ignore sweeps tracked matches out — the CLI
|
|
504
|
+
// writes the line, and upgrade tells the user the one-time sweep command.
|
|
505
|
+
function ensureGitignore(target, line) {
|
|
506
|
+
const p = path.join(target, ".gitignore");
|
|
507
|
+
let text = "";
|
|
508
|
+
if (fs.existsSync(p)) text = fs.readFileSync(p, "utf8");
|
|
509
|
+
const hit = new RegExp(`^${line.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`, "m");
|
|
510
|
+
if (hit.test(text)) return false;
|
|
511
|
+
const glue = text && !text.endsWith("\n") ? "\n" : "";
|
|
512
|
+
fs.writeFileSync(p, `${text}${glue}# 0.4 layout shim (retires in 0.5)\n${line}\n`, "utf8");
|
|
513
|
+
return true;
|
|
201
514
|
}
|
|
202
515
|
|
|
203
516
|
function ensureAgents(target, name) {
|
|
@@ -253,7 +566,7 @@ function pythonOrFail() {
|
|
|
253
566
|
|
|
254
567
|
function kernel(target, kernelArgs) {
|
|
255
568
|
const py = pythonOrFail();
|
|
256
|
-
const script = path.join(target, ".
|
|
569
|
+
const script = path.join(target, ".owfa", "scripts", "owfa_kernel.py");
|
|
257
570
|
if (!fs.existsSync(script)) fail(`no kernel at ${rel(target, script)} — is this an OWFA instance?`);
|
|
258
571
|
const res = spawnSync(py.cmd, [...py.args, script, ...kernelArgs], {
|
|
259
572
|
cwd: target,
|
|
@@ -288,11 +601,12 @@ function timestamp() {
|
|
|
288
601
|
|
|
289
602
|
|
|
290
603
|
function pendingPackages(target) {
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
604
|
+
for (const dir of [".owfa", LEGACY_META]) {
|
|
605
|
+
try {
|
|
606
|
+
return Object.keys(JSON.parse(fs.readFileSync(path.join(target, dir, "migrations", "pending.json"), "utf8")));
|
|
607
|
+
} catch {}
|
|
295
608
|
}
|
|
609
|
+
return [];
|
|
296
610
|
}
|
|
297
611
|
|
|
298
612
|
function reportPending(target) {
|
|
@@ -300,7 +614,7 @@ function reportPending(target) {
|
|
|
300
614
|
if (!pending.length) return false;
|
|
301
615
|
out("");
|
|
302
616
|
out(`semantic migration pending: ${pending.join(", ")} — the package is installed, but its first-run step is an agent act, not a mechanical one.`);
|
|
303
|
-
out(` run: python .
|
|
617
|
+
out(` run: python .owfa/scripts/owfa_kernel.py migrate ${pending[0]}`);
|
|
304
618
|
out(" (read the printed page, perform the step, then rerun with --clear)");
|
|
305
619
|
return true;
|
|
306
620
|
}
|
|
@@ -312,35 +626,114 @@ function reportPending(target) {
|
|
|
312
626
|
function cmdInit(args) {
|
|
313
627
|
const target = path.resolve(args._[0] || ".");
|
|
314
628
|
const force = args.flags.has("force");
|
|
315
|
-
const
|
|
316
|
-
|
|
629
|
+
const hasOwfa = fs.existsSync(path.join(target, ".owfa"));
|
|
630
|
+
const hasLegacyMeta = fs.existsSync(path.join(target, LEGACY_META));
|
|
631
|
+
if (hasLegacyMeta && !hasOwfa) {
|
|
317
632
|
fail(
|
|
318
|
-
`${target}
|
|
633
|
+
`${target} uses the 0.3 layout (.meta/) — run \`owfa upgrade\` to migrate to the .owfa layout ` +
|
|
634
|
+
`(the upgrade preserves all instance state and leaves a .meta shim for old paths).`
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
if (hasOwfa && !force) {
|
|
638
|
+
fail(
|
|
639
|
+
`${target} already contains .owfa/ — refusing to overwrite. Use \`owfa upgrade\` to update the framework, ` +
|
|
319
640
|
`or pass --force to re-copy the framework trees (instance data is never touched either way).`
|
|
320
641
|
);
|
|
321
642
|
}
|
|
322
643
|
const name = args.name || path.basename(target) || "workspace";
|
|
323
644
|
fs.mkdirSync(target, { recursive: true });
|
|
324
645
|
|
|
646
|
+
// two-stage bootstrap (v0.4 core de-parasitization, final shape). Stage ①: the
|
|
647
|
+
// kernel payload. Stage ② (after convergence): the personal suite — by default
|
|
648
|
+
// the blessed copy embedded in this channel tarball (offline, --path channel);
|
|
649
|
+
// `--core none` skips it; `--core <pkg-id>` takes the suite from the registry.
|
|
650
|
+
// Defensive compat branch: this channel stopped shipping .owfa/plugins/core/ in
|
|
651
|
+
// v0.4.0 — if a future (or hand-assembled) channel tree ever carries core again,
|
|
652
|
+
// the suite already lives in the framework tree and stage ② must not double-
|
|
653
|
+
// install; saying otherwise is refused up front.
|
|
654
|
+
const coreShipped =
|
|
655
|
+
fs.existsSync(path.join(CLI_ROOT, ".owfa", "plugins", "core")) ||
|
|
656
|
+
fs.existsSync(path.join(CLI_ROOT, ".meta", "plugins", "core"));
|
|
657
|
+
if (coreShipped && args.core && args.core !== "none") {
|
|
658
|
+
fail(
|
|
659
|
+
`--core ${args.core}: this channel still ships the core suite inside the framework tree ` +
|
|
660
|
+
`(compat period) — suite install is unavailable until the channel stops shipping core`
|
|
661
|
+
);
|
|
662
|
+
}
|
|
663
|
+
|
|
325
664
|
out(`OWFA init — framework ${FRAMEWORK_VERSION} → ${target}`);
|
|
326
|
-
if (!
|
|
665
|
+
if (!hasOwfa && fs.existsSync(path.join(target, ".agents", "skills"))) {
|
|
327
666
|
out("note: .agents/skills/ exists — framework skill copies are updated in place, other skills stay untouched");
|
|
328
667
|
}
|
|
329
|
-
if (
|
|
330
|
-
copyPayload(target, { replace:
|
|
331
|
-
|
|
668
|
+
if (hasOwfa) out("warning: --force replaces the framework trees; instance vault/ and wiki/ are untouched");
|
|
669
|
+
copyPayload(target, { replace: hasOwfa });
|
|
670
|
+
// P1 (review): the shim is part of the 0.4 layout for EVERY instance shape —
|
|
671
|
+
// released 0.3 packages (cuhksz/lark/github) still instruct .meta/ paths,
|
|
672
|
+
// fresh instances included.
|
|
673
|
+
if (ensureMetaShim(target) === "created") {
|
|
674
|
+
out("shim: .meta → .owfa written — 0.3-era package paths keep working (retires in 0.5)");
|
|
675
|
+
}
|
|
332
676
|
out(`agents.md: ${ensureAgents(target, name)}`);
|
|
333
677
|
|
|
334
678
|
out("");
|
|
335
|
-
out("converging projections (python .
|
|
679
|
+
out("converging projections (python .owfa/scripts/owfa_kernel.py all)");
|
|
336
680
|
if (kernel(target, ["all"]) !== 0) {
|
|
337
681
|
const pending = pendingPackages(target);
|
|
338
682
|
if (pending.length) {
|
|
339
|
-
fail(`convergence blocked by pending semantic migrations (${pending.join(", ")}) — clear them with \`python .
|
|
683
|
+
fail(`convergence blocked by pending semantic migrations (${pending.join(", ")}) — clear them with \`python .owfa/scripts/owfa_kernel.py migrate <pkg>\``);
|
|
340
684
|
}
|
|
341
685
|
fail("kernel convergence failed — the framework tree is incomplete; nothing was deleted, fix and rerun");
|
|
342
686
|
}
|
|
343
687
|
|
|
688
|
+
// ---- stage ②: the core suite ----
|
|
689
|
+
let suiteNote;
|
|
690
|
+
if (coreShipped) {
|
|
691
|
+
suiteNote = "core suite ships inside the framework tree (defensive compat branch — this channel does not)";
|
|
692
|
+
out(`suite: ${suiteNote} — no separate suite package is installed`);
|
|
693
|
+
} else if (args.core === "none") {
|
|
694
|
+
suiteNote = "no suite installed — this instance cannot install packages that depend on core capabilities until a suite is installed";
|
|
695
|
+
out(`suite: ${suiteNote}`);
|
|
696
|
+
} else if (args.core) {
|
|
697
|
+
out("");
|
|
698
|
+
out(`installing core suite ${args.core} from the registry (no --yes — the trust gate stays interactive)`);
|
|
699
|
+
if (kernel(target, ["install", args.core]) !== 0) {
|
|
700
|
+
fail(`suite package ${args.core} failed to install — the framework itself is in place; run \`owfa doctor\``);
|
|
701
|
+
}
|
|
702
|
+
suiteNote = `suite ${args.core} installed from the registry`;
|
|
703
|
+
if (reportPending(target)) {
|
|
704
|
+
fail(`framework and suite ${args.core} are installed, but the workspace is not converged until the semantic migration above is cleared`);
|
|
705
|
+
}
|
|
706
|
+
} else {
|
|
707
|
+
// default: the embedded blessed copy — 100% offline, local --path trust.
|
|
708
|
+
// A copy without plugin masters is the placeholder state (suite repo not yet
|
|
709
|
+
// synced into the channel): report it exactly like --core none. A --force
|
|
710
|
+
// re-init over an instance that already carries the suite keeps it (the
|
|
711
|
+
// package tree is instance state, the channel never replaces it here).
|
|
712
|
+
const pkgDir = path.join(target, ".owfa", "plugins", "domains", "personal");
|
|
713
|
+
const hasMasters =
|
|
714
|
+
fs.existsSync(path.join(BLESSED_SUITE, "PKG.yaml")) &&
|
|
715
|
+
fs.readdirSync(BLESSED_SUITE, { withFileTypes: true }).some(
|
|
716
|
+
(d) => d.isDirectory() && fs.existsSync(path.join(BLESSED_SUITE, d.name, "PLUGIN.yaml"))
|
|
717
|
+
);
|
|
718
|
+
if (fs.existsSync(pkgDir)) {
|
|
719
|
+
suiteNote = "personal suite already installed — kept";
|
|
720
|
+
out(`suite: ${suiteNote}`);
|
|
721
|
+
} else if (hasMasters) {
|
|
722
|
+
out("");
|
|
723
|
+
out("installing the blessed personal suite (embedded copy — offline)");
|
|
724
|
+
if (kernel(target, ["install", "personal", "--path", BLESSED_SUITE]) !== 0) {
|
|
725
|
+
fail("the embedded personal suite failed to install — the channel tarball is inconsistent; report this (the framework itself is in place)");
|
|
726
|
+
}
|
|
727
|
+
suiteNote = "personal suite installed (embedded blessed copy)";
|
|
728
|
+
if (reportPending(target)) {
|
|
729
|
+
fail("framework and suite are installed, but the workspace is not converged until the semantic migration above is cleared");
|
|
730
|
+
}
|
|
731
|
+
} else {
|
|
732
|
+
suiteNote = "no suite installed — this instance cannot install packages that depend on core capabilities until a suite is installed (this channel build carries no populated embedded suite)";
|
|
733
|
+
out(`suite: ${suiteNote}`);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
344
737
|
let installed = 0;
|
|
345
738
|
if (args.packages) {
|
|
346
739
|
for (const pkg of String(args.packages)
|
|
@@ -349,7 +742,9 @@ function cmdInit(args) {
|
|
|
349
742
|
.filter(Boolean)) {
|
|
350
743
|
out("");
|
|
351
744
|
out(`installing package ${pkg} from the registry`);
|
|
352
|
-
|
|
745
|
+
// no --yes forwarding (v0.4): auto-installable trust levels pass on their
|
|
746
|
+
// own; anything else must go through the kernel's interactive trust gate
|
|
747
|
+
const installArgs = ["install", pkg];
|
|
353
748
|
if (args.without) installArgs.push("--without", String(args.without));
|
|
354
749
|
if (kernel(target, installArgs) !== 0) fail(`package ${pkg} failed to install (earlier packages stay installed)`);
|
|
355
750
|
installed++;
|
|
@@ -358,12 +753,12 @@ function cmdInit(args) {
|
|
|
358
753
|
}
|
|
359
754
|
|
|
360
755
|
out("");
|
|
361
|
-
out(`done — framework ${FRAMEWORK_VERSION}${installed ? ` + ${installed} package(s)` : ""} in ${target}`);
|
|
756
|
+
out(`done — framework ${FRAMEWORK_VERSION}${installed ? ` + ${installed} package(s)` : ""}; ${suiteNote} in ${target}`);
|
|
362
757
|
out("");
|
|
363
758
|
out("next:");
|
|
364
759
|
out(` cd ${target}`);
|
|
365
760
|
out(" git init && git add -A && git commit -m \"init: OWFA workspace\" # recommended: instance snapshots");
|
|
366
|
-
out(
|
|
761
|
+
out(` skeleton dirs (vault/, wiki/...) came with the suite — its PKG.yaml \`skeleton:\` declared them`);
|
|
367
762
|
out(" owfa doctor # anytime: python, framework, kernel validate");
|
|
368
763
|
return 0;
|
|
369
764
|
}
|
|
@@ -371,44 +766,64 @@ function cmdInit(args) {
|
|
|
371
766
|
function cmdUpgrade(args) {
|
|
372
767
|
const target = path.resolve(args._[0] || ".");
|
|
373
768
|
const local = readVersion(target);
|
|
374
|
-
if (!local) fail(`${target} is not an OWFA instance (no .meta/VERSION) — run \`owfa init\` first`);
|
|
769
|
+
if (!local) fail(`${target} is not an OWFA instance (no .owfa/VERSION or .meta/VERSION) — run \`owfa init\` first`);
|
|
770
|
+
const legacyLayout = !fs.existsSync(path.join(target, ".owfa", "VERSION"));
|
|
375
771
|
const incoming = FRAMEWORK_VERSION;
|
|
376
772
|
const cmp = cmpVersion(local, incoming);
|
|
377
773
|
const frozen = args.flags.has("check") || args.flags.has("dry-run");
|
|
378
774
|
const force = args.flags.has("force");
|
|
379
775
|
|
|
380
776
|
out(`OWFA upgrade — instance ${local} → channel ${incoming} (CLI ${PKG.version})`);
|
|
777
|
+
if (legacyLayout) {
|
|
778
|
+
out(`layout migration: 0.3 instance (.meta/) → 0.4 layout (.owfa/ + .meta shim for old paths)`);
|
|
779
|
+
}
|
|
381
780
|
if (cmp > 0) {
|
|
382
781
|
if (!force) fail(`instance framework ${local} is newer than this CLI's ${incoming} — run \`npx owfa@latest upgrade\``);
|
|
383
782
|
out(`warning: downgrading framework ${local} → ${incoming} (--force)`);
|
|
384
783
|
}
|
|
385
|
-
if (cmp === 0 && !force) {
|
|
784
|
+
if (cmp === 0 && !force && !legacyLayout) {
|
|
386
785
|
out("already at this framework version — nothing to do (pass --force to re-sync the trees)");
|
|
387
786
|
return 0;
|
|
388
787
|
}
|
|
389
788
|
if (frozen) {
|
|
390
|
-
out(`would replace ${PAYLOAD.join(" + ")} and rerun kernel all (dry run)`);
|
|
789
|
+
out(`would replace ${PAYLOAD.join(" + ")}${legacyLayout ? " (migrating .meta/ → .owfa/)" : ""} and rerun kernel all (dry run)`);
|
|
391
790
|
return 0;
|
|
392
791
|
}
|
|
393
792
|
|
|
394
793
|
if (args.flags.has("backup")) {
|
|
395
|
-
const dir = path.join(target,
|
|
794
|
+
const dir = path.join(target, BACKUP_DIR, `${local}-${timestamp()}`);
|
|
396
795
|
for (const entry of PAYLOAD) {
|
|
397
|
-
const src = path.join(target, entry);
|
|
796
|
+
const src = path.join(target, entry === ".owfa" && legacyLayout ? LEGACY_META : entry);
|
|
398
797
|
if (fs.existsSync(src)) fs.cpSync(src, path.join(dir, entry), { recursive: true, force: true });
|
|
399
798
|
}
|
|
400
|
-
out(`backup: ${rel(target, dir)} (add .owfa/ to the instance .gitignore if you do not want it tracked)`);
|
|
799
|
+
out(`backup: ${rel(target, dir)} (add .owfa-backup/ to the instance .gitignore if you do not want it tracked)`);
|
|
401
800
|
}
|
|
402
801
|
|
|
403
802
|
out(`warning: local edits under ${PAYLOAD.join("/, ")}/ are replaced by the channel's tree — keep instance hotfixes in git`);
|
|
404
803
|
copyPayload(target, { replace: true });
|
|
804
|
+
// v0.4 core de-parasitization: if the replace rescued a parked core/ (the
|
|
805
|
+
// channel stopped shipping it), convert it into the personal suite package
|
|
806
|
+
// BEFORE converging — the conversion's own install converges the adopted set.
|
|
807
|
+
convertCoreToPersonal(target);
|
|
808
|
+
// P2 (review): self-heal a stale shim here too — instance dirs get renamed and
|
|
809
|
+
// copied, and Windows junction targets are absolute. The .gitignore sweep hint
|
|
810
|
+
// covers the "tracked twice" half only the user's git can perform.
|
|
811
|
+
const shimState = ensureMetaShim(target);
|
|
812
|
+
if (legacyLayout) {
|
|
813
|
+
if (shimState === "created" || shimState === "recreated") {
|
|
814
|
+
out(`shim: .meta → .owfa ${shimState} — 0.3-era paths keep resolving until the shim retires (0.5)`);
|
|
815
|
+
}
|
|
816
|
+
if (shimState !== "failed") {
|
|
817
|
+
out("instance git: if .meta/ was tracked before 0.4, sweep it once — git rm -r --cached .meta");
|
|
818
|
+
}
|
|
819
|
+
}
|
|
405
820
|
|
|
406
821
|
out("");
|
|
407
|
-
out("converging projections (python .
|
|
822
|
+
out("converging projections (python .owfa/scripts/owfa_kernel.py all)");
|
|
408
823
|
if (kernel(target, ["all"]) !== 0) {
|
|
409
824
|
const pending = pendingPackages(target);
|
|
410
825
|
if (pending.length) {
|
|
411
|
-
fail(`trees replaced, but convergence is blocked by pending semantic migrations (${pending.join(", ")}) — run \`python .
|
|
826
|
+
fail(`trees replaced, but convergence is blocked by pending semantic migrations (${pending.join(", ")}) — run \`python .owfa/scripts/owfa_kernel.py migrate <pkg>\``);
|
|
412
827
|
}
|
|
413
828
|
fail("kernel convergence failed after the tree replacement — the instance git snapshot is your rollback");
|
|
414
829
|
}
|
|
@@ -416,10 +831,10 @@ function cmdUpgrade(args) {
|
|
|
416
831
|
const pending = pendingPackages(target);
|
|
417
832
|
if (pending.length) {
|
|
418
833
|
out("");
|
|
419
|
-
out(`pending semantic migrations: ${pending.join(", ")} — run \`python .
|
|
834
|
+
out(`pending semantic migrations: ${pending.join(", ")} — run \`python .owfa/scripts/owfa_kernel.py migrate <pkg>\` and follow the page`);
|
|
420
835
|
}
|
|
421
836
|
out("");
|
|
422
|
-
out(`done — framework ${local} → ${incoming}`);
|
|
837
|
+
out(`done — framework ${local} → ${incoming}${legacyLayout ? " (layout migrated to .owfa/)" : ""}`);
|
|
423
838
|
return 0;
|
|
424
839
|
}
|
|
425
840
|
|
|
@@ -431,9 +846,44 @@ function cmdDoctor(args) {
|
|
|
431
846
|
out(`node ${process.version}`);
|
|
432
847
|
out(`python ${py ? py.version || `${py.cmd} ${py.args.join(" ")}` : "NOT FOUND"}`);
|
|
433
848
|
out(`target ${target}`);
|
|
434
|
-
if (!local) fail("not an OWFA instance (no .
|
|
849
|
+
if (!local) fail("not an OWFA instance (no .owfa/VERSION) — run `owfa init` first");
|
|
435
850
|
out(`instance framework ${local}`);
|
|
436
851
|
const problems = []; // required conditions — any hit fails doctor (review P2, PR #53)
|
|
852
|
+
// layout-shim health (review P2 on the layout PR): a renamed/copied instance
|
|
853
|
+
// leaves the Windows junction pointing at the old absolute path; a real .meta
|
|
854
|
+
// directory means the 0.3 layout was never migrated. doctor reports; upgrade heals.
|
|
855
|
+
const owfaDir = path.join(target, ".owfa");
|
|
856
|
+
const metaPath = path.join(target, LEGACY_META);
|
|
857
|
+
if (fs.existsSync(owfaDir)) {
|
|
858
|
+
// lstat, not existsSync: a dangling shim must reach the stale branch, not
|
|
859
|
+
// silently skip the whole check (review P2, PR #57 round 2).
|
|
860
|
+
let mst = null;
|
|
861
|
+
try {
|
|
862
|
+
mst = fs.lstatSync(metaPath);
|
|
863
|
+
} catch {
|
|
864
|
+
mst = null;
|
|
865
|
+
}
|
|
866
|
+
if (!mst) {
|
|
867
|
+
out(".meta: shim missing — 0.3-era .meta/... paths will not resolve; run `owfa upgrade` to re-create it");
|
|
868
|
+
problems.push(".meta shim is missing");
|
|
869
|
+
} else if (!mst.isSymbolicLink()) {
|
|
870
|
+
out(".meta: real directory — 0.3 layout never migrated; run `owfa upgrade`");
|
|
871
|
+
problems.push(".meta is not the layout shim");
|
|
872
|
+
} else {
|
|
873
|
+
let same = false;
|
|
874
|
+
try {
|
|
875
|
+
same = fs.realpathSync(metaPath) === fs.realpathSync(owfaDir);
|
|
876
|
+
} catch {
|
|
877
|
+
same = false; // dangling
|
|
878
|
+
}
|
|
879
|
+
if (!same) {
|
|
880
|
+
out(".meta: shim resolves outside this instance — run `owfa upgrade` to re-create it");
|
|
881
|
+
problems.push(".meta shim target is stale");
|
|
882
|
+
} else {
|
|
883
|
+
out(".meta shim: ok (resolves into this instance)");
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
}
|
|
437
887
|
const agents = path.join(target, "AGENTS.md");
|
|
438
888
|
if (!fs.existsSync(agents)) {
|
|
439
889
|
out("AGENTS.md: MISSING — the agent will not see the structural contract");
|
|
@@ -493,10 +943,14 @@ function usage() {
|
|
|
493
943
|
the owfa framework's install and upgrade channel
|
|
494
944
|
|
|
495
945
|
usage:
|
|
496
|
-
owfa init [dir] [--name NAME] [--packages a,b] [--force]
|
|
497
|
-
deploy a fresh workspace (framework ${FRAMEWORK_VERSION}); additive to existing content
|
|
946
|
+
owfa init [dir] [--name NAME] [--core none|<pkg-id>] [--packages a,b] [--force]
|
|
947
|
+
deploy a fresh workspace (framework ${FRAMEWORK_VERSION}); additive to existing content;
|
|
948
|
+
two-stage: kernel payload, then the core suite (default: embedded blessed copy,
|
|
949
|
+
offline; --core none skips it; --core <pkg-id> installs from the registry)
|
|
498
950
|
owfa upgrade [dir] [--check] [--backup] [--force]
|
|
499
|
-
replace the framework trees with this channel's version and converge
|
|
951
|
+
replace the framework trees with this channel's version and converge;
|
|
952
|
+
if this channel stopped shipping core, the instance's core plugins are
|
|
953
|
+
converted into the personal suite package (mechanical adoption)
|
|
500
954
|
owfa doctor [dir] python / framework / AGENTS.md / kernel validate
|
|
501
955
|
owfa add <pkg> [--yes] [--without a,b]
|
|
502
956
|
install a domain package from the registry, then converge
|
|
@@ -509,19 +963,25 @@ notes:
|
|
|
509
963
|
- python 3.9+ is required to run the framework; node is only the installer
|
|
510
964
|
- \`init\` never deletes instance data; \`upgrade\` preserves the kernel's
|
|
511
965
|
migration ledger, journal and registry cache
|
|
966
|
+
- skeleton dirs (vault/, wiki/...) come from package declarations (PKG.yaml
|
|
967
|
+
\`skeleton:\`), created on install — the default init already installed the
|
|
968
|
+
embedded personal suite and its skeleton
|
|
512
969
|
- no install/postinstall scripts: npm install executes nothing
|
|
513
970
|
`);
|
|
514
971
|
}
|
|
515
972
|
|
|
516
973
|
const COMMAND_HELP = {
|
|
517
|
-
init: "usage: owfa init [dir] [--name NAME] [--packages a,b] [--without m,n] [--force]\n\n" +
|
|
974
|
+
init: "usage: owfa init [dir] [--name NAME] [--core none|<pkg-id>] [--packages a,b] [--without m,n] [--force]\n\n" +
|
|
518
975
|
"deploy a fresh workspace (additive: existing files and AGENTS.md are preserved).\n" +
|
|
519
976
|
"python 3.9+ required for the framework; node 16.7+ runs this installer only.\n" +
|
|
520
|
-
"
|
|
977
|
+
"two-stage bootstrap: ① kernel payload ② core suite — default the blessed copy embedded\n" +
|
|
978
|
+
"in the channel tarball (offline); --core none skips stage ②; --core <pkg-id> installs\n" +
|
|
979
|
+
"the suite from the registry (online; the kernel's trust gate stays interactive).\n" +
|
|
980
|
+
"--packages installs further registry packages; --without prunes members (dependency-closed subsets only).\n" +
|
|
521
981
|
"--force re-copies the framework trees over an existing instance (instance data is never touched).",
|
|
522
982
|
upgrade: "usage: owfa upgrade [dir] [--check] [--backup] [--force]\n\n" +
|
|
523
983
|
"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
|
|
984
|
+
"--check (alias --dry-run) is read-only; --backup snapshots the current trees to .owfa-backup/.\n" +
|
|
525
985
|
"instance data, installed packages, pending migrations and standalone skills are preserved.",
|
|
526
986
|
doctor: "usage: owfa doctor [dir]\n\n" +
|
|
527
987
|
"health check: python, framework version, AGENTS.md injection markers, kernel validate.\n" +
|