hypomnema 1.7.4 → 1.8.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +1202 -0
- package/README.ko.md +4 -4
- package/README.md +5 -5
- package/commands/audit.md +1 -1
- package/commands/capture.md +1 -1
- package/commands/crystallize.md +3 -3
- package/commands/doctor.md +15 -1
- package/commands/feedback.md +1 -1
- package/commands/graph.md +1 -1
- package/commands/ingest.md +1 -1
- package/commands/init.md +2 -2
- package/commands/lint.md +1 -1
- package/commands/query.md +1 -1
- package/commands/rename.md +1 -1
- package/commands/resume.md +1 -1
- package/commands/stats.md +1 -1
- package/commands/uninstall.md +1 -1
- package/commands/upgrade.md +2 -2
- package/commands/verify.md +1 -1
- package/docs/ARCHITECTURE.md +8 -3
- package/docs/CONTRIBUTING.md +2 -1
- package/hooks/base-store.mjs +198 -0
- package/hooks/close-gate-store.mjs +3 -2
- package/hooks/hypo-auto-minimal-crystallize.mjs +10 -0
- package/hooks/hypo-compact-guard.mjs +5 -3
- package/hooks/hypo-cwd-change.mjs +8 -0
- package/hooks/hypo-personal-check.mjs +127 -67
- package/hooks/hypo-session-start.mjs +91 -10
- package/hooks/hypo-shared.mjs +241 -19
- package/hooks/version-check.mjs +44 -6
- package/package.json +9 -3
- package/scripts/capture.mjs +49 -17
- package/scripts/crystallize.mjs +20 -2092
- package/scripts/doctor.mjs +334 -14
- package/scripts/init.mjs +106 -81
- package/scripts/lib/crystallize-args.mjs +50 -0
- package/scripts/lib/crystallize-close-apply.mjs +1830 -0
- package/scripts/lib/crystallize-close-check.mjs +238 -0
- package/scripts/lib/crystallize-close-gate.mjs +58 -0
- package/scripts/lib/crystallize-helpers.mjs +55 -0
- package/scripts/lib/extensions.mjs +175 -49
- package/scripts/lib/git-hooks-dir.mjs +88 -3
- package/scripts/lib/plugin-detect.mjs +176 -21
- package/scripts/lint.mjs +12 -6
- package/scripts/proposal.mjs +3 -3
- package/scripts/upgrade.mjs +243 -20
- package/skills/crystallize/SKILL.md +15 -13
- package/skills/graph/SKILL.md +3 -3
- package/skills/ingest/SKILL.md +2 -2
- package/skills/lint/SKILL.md +3 -3
- package/skills/query/SKILL.md +3 -3
- package/skills/verify/SKILL.md +3 -3
- package/templates/hypo-automation.md +59 -17
- package/templates/hypo-config.md +1 -1
- package/templates/hypo-guide.md +12 -9
- package/templates/hypo-help.md +1 -1
package/scripts/init.mjs
CHANGED
|
@@ -32,7 +32,7 @@ import {
|
|
|
32
32
|
renameSync,
|
|
33
33
|
unlinkSync,
|
|
34
34
|
} from 'fs';
|
|
35
|
-
import { join, basename, dirname
|
|
35
|
+
import { join, basename, dirname } from 'path';
|
|
36
36
|
import { homedir } from 'os';
|
|
37
37
|
import { execSync, spawnSync } from 'child_process';
|
|
38
38
|
import { fileURLToPath } from 'url';
|
|
@@ -47,6 +47,7 @@ import {
|
|
|
47
47
|
SHELL_MARKER_START,
|
|
48
48
|
SHELL_MARKER_END,
|
|
49
49
|
SHELL_FUNCTION_BODY,
|
|
50
|
+
wikiPreCommitContent,
|
|
50
51
|
} from './lib/git-hooks-dir.mjs';
|
|
51
52
|
import { readCoreHooksConfig } from './lib/core-hooks.mjs';
|
|
52
53
|
import {
|
|
@@ -60,11 +61,7 @@ import { syncExtensions } from './lib/extensions.mjs';
|
|
|
60
61
|
import { writeProvenanceSidecar } from './lib/pkg-provenance.mjs';
|
|
61
62
|
import { templateSchemaVersion } from './lib/template-schema-version.mjs';
|
|
62
63
|
import { classifyInstall, downgradeGuardMessage } from '../hooks/version-check.mjs';
|
|
63
|
-
import {
|
|
64
|
-
isHypomnemaPluginEnabled,
|
|
65
|
-
resolveEnabledPluginRoot as resolveEnabledPluginRootShared,
|
|
66
|
-
usablePkgRoot,
|
|
67
|
-
} from './lib/plugin-detect.mjs';
|
|
64
|
+
import { resolvePluginChannel, usablePkgRoot } from './lib/plugin-detect.mjs';
|
|
68
65
|
|
|
69
66
|
const HOME = homedir();
|
|
70
67
|
const SCRIPT_DIR = fileURLToPath(new URL('.', import.meta.url));
|
|
@@ -756,50 +753,12 @@ function installPkgGitHook(dryRun) {
|
|
|
756
753
|
}
|
|
757
754
|
|
|
758
755
|
// ── wiki pre-commit hook ─────────────────────────────────────────────────────
|
|
759
|
-
|
|
760
|
-
// Single-quote escaping prevents shell expansion of special chars (e.g. $HOME, backticks) in path
|
|
761
|
-
function shellSingleQuote(p) {
|
|
762
|
-
return `'${p.replace(/'/g, "'\\''")}'`;
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
// `root` is the DURABLE install root the hook should resolve hypo-pre-commit.mjs
|
|
766
|
-
// (and, when opted in, lint.mjs) through — not necessarily PKG_ROOT. In a dual
|
|
767
|
-
// install (a manual/npm init while the plugin is enabled) PKG_ROOT is the
|
|
768
|
-
// manual/npm checkout the dual-install notice tells the user to uninstall;
|
|
769
|
-
// embedding it here would leave the vault's git hook dangling the moment they
|
|
770
|
-
// do, breaking every wiki commit. The caller passes the preserved plugin-owned
|
|
771
|
-
// root instead (see `durableRoot`), so the hook points at the install that
|
|
772
|
-
// actually persists.
|
|
773
756
|
//
|
|
774
|
-
//
|
|
775
|
-
//
|
|
776
|
-
//
|
|
777
|
-
//
|
|
778
|
-
//
|
|
779
|
-
// install-git-hooks.mjs — so toggling it means re-running init with/without
|
|
780
|
-
// `--lint-strict`, not editing the hook by hand.
|
|
781
|
-
//
|
|
782
|
-
// `hypoDir` MUST be absolutized before it is baked in. Git runs a pre-commit
|
|
783
|
-
// hook with cwd set to the wiki's own working-tree root, so a relative
|
|
784
|
-
// `--hypo-dir` (e.g. from `hypo init --hypo-dir=wiki` run from its parent) is
|
|
785
|
-
// re-resolved AT COMMIT TIME against that root instead of the directory the
|
|
786
|
-
// caller meant — `wiki` becomes `<wiki-root>/wiki`, a path that doesn't exist,
|
|
787
|
-
// and lint.mjs falls through to its own default resolution (HYPO_DIR/
|
|
788
|
-
// hypo-config.md scan) and may silently lint an unrelated vault. `worker`
|
|
789
|
-
// needs no such treatment: `root` is already realpath'd upstream (see
|
|
790
|
-
// `durableRoot` / PKG_ROOT resolution), so it's absolute at every call site.
|
|
791
|
-
function wikiPreCommitContent(root, hypoDir, lintStrict) {
|
|
792
|
-
const absHypoDir = resolve(hypoDir);
|
|
793
|
-
const worker = join(root, 'hooks', 'hypo-pre-commit.mjs');
|
|
794
|
-
const steps = [`node ${shellSingleQuote(worker)} || exit 1`];
|
|
795
|
-
if (lintStrict) {
|
|
796
|
-
const lintScript = join(root, 'scripts', 'lint.mjs');
|
|
797
|
-
steps.push(
|
|
798
|
-
`node ${shellSingleQuote(lintScript)} --hypo-dir=${shellSingleQuote(absHypoDir)} --strict || exit 1`,
|
|
799
|
-
);
|
|
800
|
-
}
|
|
801
|
-
return `#!/bin/sh\n${WIKI_PRE_COMMIT_MARKER_START}\n${steps.join('\n')}\nexit 0\n${WIKI_PRE_COMMIT_MARKER_END}\n`;
|
|
802
|
-
}
|
|
757
|
+
// shellSingleQuote and wikiPreCommitContent live in lib/git-hooks-dir.mjs, not
|
|
758
|
+
// here: upgrade.mjs's self-heal (repointing an existing hook at the current
|
|
759
|
+
// durable root) and doctor.mjs's drift check both need to build/parse the
|
|
760
|
+
// exact same body this writer produces, so one copy is shared rather than
|
|
761
|
+
// three independent copies silently drifting apart.
|
|
803
762
|
|
|
804
763
|
function installWikiPreCommitHook(hypoDir, dryRun, force, root, lintStrict) {
|
|
805
764
|
const { dir: hooksDir, skip } = hooksDirForInstall(hypoDir);
|
|
@@ -995,11 +954,16 @@ function gitSetup(hypoDir, remote, dryRun) {
|
|
|
995
954
|
|
|
996
955
|
// ── first commit + push ──────────────────────────────────────────────────────
|
|
997
956
|
|
|
957
|
+
// Returns true only when THIS run actually created a commit. The
|
|
958
|
+
// channel-unresolved notice keys its "the commit this run just made was
|
|
959
|
+
// unguarded" sentence off that, so a repo that already had commits, a dry run,
|
|
960
|
+
// and a failed commit must all read as false — none of them produced an
|
|
961
|
+
// unguarded commit to warn about.
|
|
998
962
|
function firstCommit(hypoDir, remote, dryRun) {
|
|
999
963
|
const logR = git(hypoDir, ['log', '--oneline', '-1']);
|
|
1000
964
|
if (logR.status === 0 && logR.stdout.trim()) {
|
|
1001
965
|
log('skipped', 'first commit (repo already has commits)');
|
|
1002
|
-
return;
|
|
966
|
+
return false;
|
|
1003
967
|
}
|
|
1004
968
|
const today = new Date().toISOString().slice(0, 10);
|
|
1005
969
|
if (!dryRun) {
|
|
@@ -1007,7 +971,7 @@ function firstCommit(hypoDir, remote, dryRun) {
|
|
|
1007
971
|
const commitR = git(hypoDir, ['commit', '-m', `chore: init hypomnema (${today})`]);
|
|
1008
972
|
if (commitR.status !== 0) {
|
|
1009
973
|
log('errors', 'first commit failed');
|
|
1010
|
-
return;
|
|
974
|
+
return false;
|
|
1011
975
|
}
|
|
1012
976
|
if (remote) {
|
|
1013
977
|
const actualOrigin = git(hypoDir, ['remote', 'get-url', 'origin']);
|
|
@@ -1018,6 +982,7 @@ function firstCommit(hypoDir, remote, dryRun) {
|
|
|
1018
982
|
}
|
|
1019
983
|
}
|
|
1020
984
|
log('created', `first commit (${today})`);
|
|
985
|
+
return !dryRun;
|
|
1021
986
|
}
|
|
1022
987
|
|
|
1023
988
|
// ── main ─────────────────────────────────────────────────────────────────────
|
|
@@ -1026,7 +991,7 @@ const args = parseArgs(process.argv);
|
|
|
1026
991
|
|
|
1027
992
|
// ── install channel ───────────────────────────────────────────────────────────
|
|
1028
993
|
//
|
|
1029
|
-
// A plugin-channel install already provides the
|
|
994
|
+
// A plugin-channel install already provides the core hooks in hooks/hooks.json plus the slash commands
|
|
1030
995
|
// through the package's own hooks/hooks.json + commands/ (Claude Code auto-wires
|
|
1031
996
|
// them). Copying the same hooks into ~/.claude/hooks and registering the same
|
|
1032
997
|
// events in settings.json on top makes every core hook fire TWICE, the same
|
|
@@ -1035,22 +1000,15 @@ const args = parseArgs(process.argv);
|
|
|
1035
1000
|
// `.claude/plugins/…` root); `hypomnemaPluginEnabled` catches the dual-install
|
|
1036
1001
|
// variant (a manual/npm init.mjs run while the plugin is ALSO enabled — see
|
|
1037
1002
|
// lib/plugin-detect.mjs, fail-open on any uncertainty). Either signal means
|
|
1038
|
-
// Claude's core hook/command surface is plugin-managed already.
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
// settings/registry paths so callers below read the same as before the move.
|
|
1048
|
-
function resolveEnabledPluginRoot(settingsPath) {
|
|
1049
|
-
return resolveEnabledPluginRootShared(
|
|
1050
|
-
settingsPath,
|
|
1051
|
-
join(HOME, '.claude', 'plugins', 'installed_plugins.json'),
|
|
1052
|
-
);
|
|
1053
|
-
}
|
|
1003
|
+
// Claude's core hook/command surface is plugin-managed already. resolvePluginChannel
|
|
1004
|
+
// is the single place this judgment is made — doctor.mjs and upgrade.mjs call the
|
|
1005
|
+
// same function with their own PKG_ROOT so all three tools agree.
|
|
1006
|
+
const pluginChannel = resolvePluginChannel({
|
|
1007
|
+
pkgRoot: PKG_ROOT,
|
|
1008
|
+
settingsPath: join(HOME, '.claude', 'settings.json'),
|
|
1009
|
+
registryPath: join(HOME, '.claude', 'plugins', 'installed_plugins.json'),
|
|
1010
|
+
});
|
|
1011
|
+
const { pluginMode, hypomnemaPluginEnabled, coreManagedByPlugin } = pluginChannel;
|
|
1054
1012
|
|
|
1055
1013
|
// Non-mutating read of the recorded pkgRoot. resolveDurableRoot runs before init's
|
|
1056
1014
|
// package validation and its "no writes before validation" guarantee, so it must
|
|
@@ -1076,13 +1034,55 @@ function recordedPkgRoot() {
|
|
|
1076
1034
|
// only install.
|
|
1077
1035
|
function resolveDurableRoot() {
|
|
1078
1036
|
if (!hypomnemaPluginEnabled) return PKG_ROOT;
|
|
1079
|
-
|
|
1080
|
-
if (pluginRoot) return pluginRoot;
|
|
1037
|
+
if (pluginChannel.root) return pluginChannel.root;
|
|
1081
1038
|
const recorded = recordedPkgRoot();
|
|
1082
1039
|
if (usablePkgRoot(recorded)) return recorded;
|
|
1083
|
-
|
|
1040
|
+
// pluginChannel.root is null here because the channel JUDGMENT FAILED (reason
|
|
1041
|
+
// 'registry-unreadable' or 'unresolved'), not because no plugin is installed —
|
|
1042
|
+
// hypomnemaPluginEnabled is already true, and that case already returned
|
|
1043
|
+
// above. Falling back to PKG_ROOT would persist a GUESS as if it were the
|
|
1044
|
+
// confirmed durable root; in a dual install PKG_ROOT is the manual/npm
|
|
1045
|
+
// checkout the dual-install notice tells the user to remove, so that guess
|
|
1046
|
+
// dangles the moment they do. A judge returning null here means "cannot
|
|
1047
|
+
// positively resolve", never "resolved to nothing usable exists" — treating
|
|
1048
|
+
// it as the latter is exactly the failure this fix closes.
|
|
1049
|
+
// With no existing usable pointer to preserve either, leave the durable root
|
|
1050
|
+
// UNRESOLVED (null) instead of guessing — the caller below skips writing the
|
|
1051
|
+
// pointer and the pre-commit hook and prints recovery guidance rather than
|
|
1052
|
+
// confirm a guess.
|
|
1053
|
+
return null;
|
|
1084
1054
|
}
|
|
1085
1055
|
const durableRoot = resolveDurableRoot();
|
|
1056
|
+
// True only when the channel judgment itself failed and there was nothing usable
|
|
1057
|
+
// to fall back on (see resolveDurableRoot above) — never true for a genuinely
|
|
1058
|
+
// disabled/absent plugin, which resolves to PKG_ROOT and never reaches here.
|
|
1059
|
+
const channelUnresolved = durableRoot === null;
|
|
1060
|
+
let madeFirstCommit = false;
|
|
1061
|
+
|
|
1062
|
+
function channelUnresolvedNotice(madeFirstCommit) {
|
|
1063
|
+
// Step 9 (firstCommit) runs AFTER step 8b skipped the pre-commit hook install,
|
|
1064
|
+
// so a commit this run created went out unguarded, not just future ones. Key
|
|
1065
|
+
// this off what firstCommit actually did, never off the flags that merely make
|
|
1066
|
+
// it eligible: --dry-run writes nothing, a repo with existing commits skips,
|
|
1067
|
+
// and a failed commit produced none. Asserting "just made" in those cases
|
|
1068
|
+
// sends the reader looking for a commit that does not exist.
|
|
1069
|
+
const firstCommitCaveat = madeFirstCommit
|
|
1070
|
+
? ' The initial commit this run just made in the vault was not checked by that guard ' +
|
|
1071
|
+
'either: it landed before any pre-commit hook existed to check it.'
|
|
1072
|
+
: '';
|
|
1073
|
+
return (
|
|
1074
|
+
`⚠ Cannot positively resolve the enabled Hypomnema plugin's install root ` +
|
|
1075
|
+
`(${pluginChannel.rootReason} — checked ~/.claude/plugins/installed_plugins.json). ` +
|
|
1076
|
+
`Leaving the durable pointer UNSET rather than guessing the npm path at ${PKG_ROOT}: ` +
|
|
1077
|
+
`that path is what a dual install's own notice tells you to remove, and baking it in ` +
|
|
1078
|
+
`now would break every wiki commit the moment you do. hypo-pkg.json's pkgRoot and the ` +
|
|
1079
|
+
`vault's git pre-commit hook are both skipped this run, so this wiki has no pre-commit ` +
|
|
1080
|
+
`.hypoignore guard until this resolves.${firstCommitCaveat}\n` +
|
|
1081
|
+
` → Fix: repair or refresh ~/.claude/plugins/installed_plugins.json (reinstall the ` +
|
|
1082
|
+
'plugin, or run `/plugin marketplace update hypomnema` then `/reload-plugins`), then ' +
|
|
1083
|
+
're-run `hypomnema init` to record the confirmed root.'
|
|
1084
|
+
);
|
|
1085
|
+
}
|
|
1086
1086
|
|
|
1087
1087
|
// Validate hooks.json before any file writes so a bad package leaves no partial state
|
|
1088
1088
|
const HOOK_MAP = args.hooks || args.codex ? loadHookMap() : null;
|
|
@@ -1206,7 +1206,20 @@ if (args.hooks) {
|
|
|
1206
1206
|
// so this never clobbers a commands/extensions map written elsewhere.
|
|
1207
1207
|
// Record hypo-pkg.json against the durable root (the plugin's real cache root in a
|
|
1208
1208
|
// dual install, PKG_ROOT otherwise), the same root the wiki pre-commit hook uses.
|
|
1209
|
-
|
|
1209
|
+
// A null durableRoot means the channel judgment failed with nothing usable to
|
|
1210
|
+
// preserve (see resolveDurableRoot) — skip the write rather than stamp a guess;
|
|
1211
|
+
// channelUnresolvedNotice() above explains why and what closes it. The
|
|
1212
|
+
// extensions sync below (step 4b) still runs unconditionally and can leave a
|
|
1213
|
+
// pkgRoot-less hypo-pkg.json on disk in this case; that is doctor.mjs's own
|
|
1214
|
+
// distinct reported state ("no pkgRoot field"), never a confirmed PKG_ROOT.
|
|
1215
|
+
if (!channelUnresolved) {
|
|
1216
|
+
writePkgJson(args.dryRun, commandSHAs ? { commands: commandSHAs } : {}, durableRoot);
|
|
1217
|
+
} else {
|
|
1218
|
+
log(
|
|
1219
|
+
'skipped',
|
|
1220
|
+
'hypo-pkg.json pkgRoot (plugin channel judgment failed — see notice above; re-run after fixing the registry)',
|
|
1221
|
+
);
|
|
1222
|
+
}
|
|
1210
1223
|
|
|
1211
1224
|
// 4b. user extensions companion sync. Runs after
|
|
1212
1225
|
// writePkgJson so the per-target SHA map is merged into the same hypo-pkg.json
|
|
@@ -1298,23 +1311,35 @@ if (args.hooks) {
|
|
|
1298
1311
|
|
|
1299
1312
|
// 8b. wiki pre-commit hook (.hypoignore last-line-of-defence guard — §6.8).
|
|
1300
1313
|
// Point it at the durable install root, not PKG_ROOT, so a dual install does not
|
|
1301
|
-
// embed a manual/npm path that dangles once the user uninstalls it.
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1314
|
+
// embed a manual/npm path that dangles once the user uninstalls it. A null
|
|
1315
|
+
// durableRoot means the channel judgment failed with nothing usable to fall
|
|
1316
|
+
// back on — skip installing the hook with a guessed root; an existing hook (if
|
|
1317
|
+
// any) is left exactly as-is, and channelUnresolvedNotice() above says what to
|
|
1318
|
+
// do next.
|
|
1319
|
+
if (!channelUnresolved) {
|
|
1320
|
+
installWikiPreCommitHook(
|
|
1321
|
+
args.hypoDir,
|
|
1322
|
+
args.dryRun,
|
|
1323
|
+
args.forceCommands,
|
|
1324
|
+
durableRoot,
|
|
1325
|
+
args.lintStrict,
|
|
1326
|
+
);
|
|
1327
|
+
} else {
|
|
1328
|
+
log(
|
|
1329
|
+
'skipped',
|
|
1330
|
+
`${args.hypoDir} pre-commit hook (plugin channel judgment failed — see notice above)`,
|
|
1331
|
+
);
|
|
1332
|
+
}
|
|
1309
1333
|
|
|
1310
1334
|
// 9. first commit + push (skip when cloned from remote — already has commits)
|
|
1311
1335
|
if (args.gitInit && !args.fromRemote) {
|
|
1312
|
-
firstCommit(args.hypoDir, args.gitRemote, args.dryRun);
|
|
1336
|
+
madeFirstCommit = firstCommit(args.hypoDir, args.gitRemote, args.dryRun);
|
|
1313
1337
|
}
|
|
1314
1338
|
|
|
1315
1339
|
// ── report ───────────────────────────────────────────────────────────────────
|
|
1316
1340
|
|
|
1317
1341
|
const lines = [];
|
|
1342
|
+
if (channelUnresolved) lines.push(channelUnresolvedNotice(madeFirstCommit), '');
|
|
1318
1343
|
if (results.created.length)
|
|
1319
1344
|
lines.push(
|
|
1320
1345
|
`✓ Created (${results.created.length}):\n${results.created.map((p) => ` ${p}`).join('\n')}`,
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { resolveHypoRoot, expandHome } from './hypo-root.mjs';
|
|
2
|
+
import { isValidProjectName } from './project-create.mjs';
|
|
3
|
+
|
|
4
|
+
// ── arg parsing ──────────────────────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
export function parseArgs(argv) {
|
|
7
|
+
const args = {
|
|
8
|
+
hypoDir: null,
|
|
9
|
+
minGroup: 2,
|
|
10
|
+
json: false,
|
|
11
|
+
checkSessionClose: false,
|
|
12
|
+
applySessionClose: false,
|
|
13
|
+
markSessionClosed: false,
|
|
14
|
+
logOnly: false,
|
|
15
|
+
sessionId: null,
|
|
16
|
+
payload: null,
|
|
17
|
+
force: false,
|
|
18
|
+
transcriptPath: null,
|
|
19
|
+
project: null,
|
|
20
|
+
};
|
|
21
|
+
for (const arg of argv.slice(2)) {
|
|
22
|
+
if (arg.startsWith('--hypo-dir=')) args.hypoDir = expandHome(arg.slice(11));
|
|
23
|
+
else if (arg.startsWith('--min-group=')) args.minGroup = parseInt(arg.slice(12), 10) || 2;
|
|
24
|
+
else if (arg === '--check-session-close') args.checkSessionClose = true;
|
|
25
|
+
else if (arg === '--apply-session-close') args.applySessionClose = true;
|
|
26
|
+
else if (arg === '--mark-session-closed') args.markSessionClosed = true;
|
|
27
|
+
else if (arg === '--log-only') args.logOnly = true;
|
|
28
|
+
else if (arg.startsWith('--session-id=')) args.sessionId = arg.slice(13);
|
|
29
|
+
else if (arg.startsWith('--payload=')) args.payload = arg.slice(10);
|
|
30
|
+
else if (arg.startsWith('--transcript-path=')) args.transcriptPath = expandHome(arg.slice(18));
|
|
31
|
+
else if (arg.startsWith('--session-cwd=')) args.sessionCwd = expandHome(arg.slice(14));
|
|
32
|
+
else if (arg.startsWith('--project=')) args.project = arg.slice(10);
|
|
33
|
+
else if (arg === '--force') args.force = true;
|
|
34
|
+
else if (arg === '--json') args.json = true;
|
|
35
|
+
}
|
|
36
|
+
if (!args.hypoDir) args.hypoDir = resolveHypoRoot();
|
|
37
|
+
// --project=<slug> override (check/mark only). Validate the SYNTAX here so a
|
|
38
|
+
// traversal/charset attack (`--project=../x`) is rejected before any path is
|
|
39
|
+
// built from it — sessionCloseFileStatus(projectOverride) joins it directly.
|
|
40
|
+
// isValidProjectName is the SHARED validator (project-create.mjs), so the
|
|
41
|
+
// override accepts exactly the namespace createProject can scaffold. Existence
|
|
42
|
+
// (a real projects/<slug>/ directory) is checked in the run functions, where
|
|
43
|
+
// hypoDir is resolved and only the check/mark paths consume --project.
|
|
44
|
+
if (args.project != null && !isValidProjectName(args.project)) {
|
|
45
|
+
const msg = `--project "${args.project}" is not a valid project name (need a single segment with ≥1 alnum, charset A-Za-z0-9._-, not "."/"..")`;
|
|
46
|
+
console.log(args.json ? JSON.stringify({ ok: false, error: msg }, null, 2) : `✗ ${msg}`);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
return args;
|
|
50
|
+
}
|