@wirecat/cli-template 0.0.0-stage → 0.1.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.
Files changed (132) hide show
  1. package/.github/dependabot.yml +23 -0
  2. package/.github/workflows/changelog-guard.yml +12 -0
  3. package/.github/workflows/ci.yml +88 -0
  4. package/.github/workflows/main.yml +17 -0
  5. package/.github/workflows/monthly.yml +28 -0
  6. package/.github/workflows/pr.yml +19 -0
  7. package/.github/workflows/release.yml +191 -0
  8. package/.github/workflows/standards.yml +51 -0
  9. package/.gitleaks.toml +5 -0
  10. package/.markdownlint-cli2.jsonc +7 -0
  11. package/.vale.ini +5 -0
  12. package/AGENTS.md +27 -0
  13. package/CHANGELOG.md +31 -0
  14. package/CLAUDE.md +17 -0
  15. package/LICENSE +204 -0
  16. package/README.md +52 -2
  17. package/SECURITY.md +15 -0
  18. package/bin/release +139 -0
  19. package/biome.json +6 -0
  20. package/config/wirecat/LICENSE +204 -0
  21. package/config/wirecat/biome.base.json +157 -0
  22. package/config/wirecat/check.mjs +41 -0
  23. package/config/wirecat/cspell.json +80 -0
  24. package/config/wirecat/lefthook.yml +26 -0
  25. package/config/wirecat/manifest.json +37 -0
  26. package/config/wirecat/markdownlint.json +10 -0
  27. package/config/wirecat/no-deep-wirecat-import.grit +15 -0
  28. package/config/wirecat/no-exec-interpolation.grit +17 -0
  29. package/config/wirecat/no-new-function.grit +8 -0
  30. package/config/wirecat/no-shell-true.grit +8 -0
  31. package/config/wirecat/no-sqlite-driver.grit +15 -0
  32. package/config/wirecat/no-unbounded-spread.grit +10 -0
  33. package/config/wirecat/release-lock-info.mjs +71 -0
  34. package/config/wirecat/release-lock.sh +34 -0
  35. package/config/wirecat/rumdl.toml +9 -0
  36. package/contracts/help.snap +16 -0
  37. package/contracts/mcp-tools.json +922 -0
  38. package/contracts/no-leak.json +3 -0
  39. package/contracts/plan.json +50 -0
  40. package/cspell.json +10 -0
  41. package/dist/bin/app.d.ts +3 -0
  42. package/dist/bin/app.d.ts.map +1 -0
  43. package/dist/bin/app.js +14 -0
  44. package/dist/bin/app.js.map +1 -0
  45. package/dist/commands/doctor.d.ts +23 -0
  46. package/dist/commands/doctor.d.ts.map +1 -0
  47. package/dist/commands/doctor.js +44 -0
  48. package/dist/commands/doctor.js.map +1 -0
  49. package/dist/commands/mcp.d.ts +3 -0
  50. package/dist/commands/mcp.d.ts.map +1 -0
  51. package/dist/commands/mcp.js +36 -0
  52. package/dist/commands/mcp.js.map +1 -0
  53. package/dist/config.d.ts +3 -0
  54. package/dist/config.d.ts.map +1 -0
  55. package/dist/config.js +4 -0
  56. package/dist/config.js.map +1 -0
  57. package/dist/identity.d.ts +5 -0
  58. package/dist/identity.d.ts.map +1 -0
  59. package/dist/identity.js +12 -0
  60. package/dist/identity.js.map +1 -0
  61. package/dist/index.d.ts +3 -0
  62. package/dist/index.d.ts.map +1 -0
  63. package/dist/index.js +3 -0
  64. package/dist/index.js.map +1 -0
  65. package/dist/mcp.d.ts +19 -0
  66. package/dist/mcp.d.ts.map +1 -0
  67. package/dist/mcp.js +62 -0
  68. package/dist/mcp.js.map +1 -0
  69. package/dist/program.d.ts +6 -0
  70. package/dist/program.d.ts.map +1 -0
  71. package/dist/program.js +37 -0
  72. package/dist/program.js.map +1 -0
  73. package/dist/version.d.ts +2 -0
  74. package/dist/version.d.ts.map +1 -0
  75. package/dist/version.js +2 -0
  76. package/dist/version.js.map +1 -0
  77. package/docs/commands.md +227 -0
  78. package/docs/configuration.md +18 -0
  79. package/docs/dev/DESIGN.md +13 -0
  80. package/docs/dev/TESTING.md +16 -0
  81. package/docs/dev/design-questions.md +24 -0
  82. package/docs/dev/reference-clis.md +15 -0
  83. package/docs/dev/skills/design-interview/SKILL.md +34 -0
  84. package/docs/dev/skills/release/SKILL.md +24 -0
  85. package/docs/dev/test-matrix.md +52 -0
  86. package/docs/index.md +7 -0
  87. package/docs/installation.md +17 -0
  88. package/docs/meta.json +18 -0
  89. package/docs/security.md +8 -0
  90. package/docs/troubleshooting.md +14 -0
  91. package/docs/usage.md +31 -0
  92. package/gitignore +44 -0
  93. package/kit/README.md +127 -0
  94. package/kit/dist/interview.js +170 -0
  95. package/kit/dist/main.js +70 -0
  96. package/kit/dist/manifest.js +111 -0
  97. package/kit/dist/new.js +256 -0
  98. package/kit/dist/rename.js +36 -0
  99. package/kit/dist/source.js +98 -0
  100. package/kit/dist/upgrade.js +230 -0
  101. package/kit/tsconfig.json +17 -0
  102. package/kit/tsconfig.test.json +5 -0
  103. package/lefthook.yml +2 -0
  104. package/lint/baseline/.gitkeep +0 -0
  105. package/lint/overrides.json +1 -0
  106. package/package.json +106 -4
  107. package/pnpm-workspace.yaml +6 -0
  108. package/scripts/audit.sh +22 -0
  109. package/scripts/command-program.ts +1 -0
  110. package/scripts/commands.ts +39 -0
  111. package/scripts/smoke.ts +56 -0
  112. package/scripts/test-matrix-untested.ts +12 -0
  113. package/skills/app-cli/SKILL.md +21 -0
  114. package/src/bin/app.ts +13 -0
  115. package/src/commands/doctor.ts +45 -0
  116. package/src/commands/mcp.ts +38 -0
  117. package/src/config.ts +4 -0
  118. package/src/identity.ts +15 -0
  119. package/src/index.ts +2 -0
  120. package/src/mcp.test.ts +84 -0
  121. package/src/mcp.ts +82 -0
  122. package/src/program.test.ts +66 -0
  123. package/src/program.ts +53 -0
  124. package/src/roots.test.ts +80 -0
  125. package/src/testing/argv-log.ts +1 -0
  126. package/src/testing/sandbox.ts +25 -0
  127. package/src/version.ts +1 -0
  128. package/template.json +40 -0
  129. package/tsconfig.json +9 -0
  130. package/tsconfig.scripts.json +1 -0
  131. package/tsconfig.test.json +1 -0
  132. package/vitest.config.ts +19 -0
@@ -0,0 +1,157 @@
1
+ {
2
+ "vcs": {
3
+ "enabled": true,
4
+ "clientKind": "git",
5
+ "useIgnoreFile": true
6
+ },
7
+ "formatter": {
8
+ "enabled": true,
9
+ "indentStyle": "space",
10
+ "indentWidth": 2,
11
+ "lineWidth": 120
12
+ },
13
+ "javascript": {
14
+ "formatter": {
15
+ "quoteStyle": "double",
16
+ "semicolons": "asNeeded",
17
+ "trailingCommas": "all",
18
+ "arrowParentheses": "always"
19
+ }
20
+ },
21
+ "linter": {
22
+ "enabled": true,
23
+ "rules": {
24
+ "preset": "recommended",
25
+ "style": {
26
+ "useConst": "error",
27
+ "noNonNullAssertion": "error",
28
+ "noExcessiveLinesPerFile": {
29
+ "level": "error",
30
+ "options": {
31
+ "maxLines": 1000
32
+ }
33
+ }
34
+ },
35
+ "correctness": {
36
+ "noUnusedImports": "error",
37
+ "noUnusedVariables": "error"
38
+ },
39
+ "suspicious": {
40
+ "noExplicitAny": "error",
41
+ "noConsole": {
42
+ "level": "error",
43
+ "options": {
44
+ "allow": ["error"]
45
+ }
46
+ },
47
+ "noImportCycles": "error"
48
+ },
49
+ "security": {
50
+ "noSecrets": {
51
+ "level": "error",
52
+ "options": {
53
+ "entropyThreshold": 50
54
+ }
55
+ }
56
+ }
57
+ }
58
+ },
59
+ "overrides": [
60
+ {
61
+ "includes": ["scripts/**"],
62
+ "linter": {
63
+ "rules": {
64
+ "suspicious": {
65
+ "noConsole": "off"
66
+ }
67
+ }
68
+ }
69
+ },
70
+ {
71
+ "includes": [
72
+ "**/*.test.*",
73
+ "**/*.spec.*",
74
+ "**/test/**",
75
+ "**/tests/**",
76
+ "**/__tests__/**",
77
+ "**/fixtures/**",
78
+ "**/generated/**"
79
+ ],
80
+ "linter": {
81
+ "rules": {
82
+ "style": {
83
+ "noExcessiveLinesPerFile": "off"
84
+ }
85
+ }
86
+ }
87
+ },
88
+ {
89
+ "includes": [
90
+ "**/*.test.*",
91
+ "**/*.spec.*",
92
+ "**/test/**",
93
+ "**/tests/**",
94
+ "**/__tests__/**",
95
+ "**/fixtures/**",
96
+ "**/generated/**"
97
+ ],
98
+ "linter": {
99
+ "rules": {
100
+ "security": {
101
+ "noSecrets": "off"
102
+ }
103
+ }
104
+ }
105
+ }
106
+ ],
107
+ "plugins": [
108
+ "./config/wirecat/no-new-function.grit",
109
+ "./config/wirecat/no-shell-true.grit",
110
+ "./config/wirecat/no-exec-interpolation.grit",
111
+ {
112
+ "path": "./config/wirecat/no-deep-wirecat-import.grit",
113
+ "includes": [
114
+ "**",
115
+ "!**/*.test.*",
116
+ "!**/*.spec.*",
117
+ "!**/test/**",
118
+ "!**/tests/**",
119
+ "!**/__tests__/**",
120
+ "!**/fixtures/**"
121
+ ]
122
+ },
123
+ {
124
+ "path": "./config/wirecat/no-sqlite-driver.grit",
125
+ "includes": [
126
+ "**",
127
+ "!**/*.test.*",
128
+ "!**/*.spec.*",
129
+ "!**/test/**",
130
+ "!**/tests/**",
131
+ "!**/__tests__/**",
132
+ "!**/fixtures/**",
133
+ "!**/testing/**",
134
+ "!**/scripts/**",
135
+ "!**/bench/**",
136
+ "!**/performance/**",
137
+ "!**/contracts/**"
138
+ ]
139
+ },
140
+ {
141
+ "path": "./config/wirecat/no-unbounded-spread.grit",
142
+ "includes": [
143
+ "**",
144
+ "!**/*.test.*",
145
+ "!**/*.spec.*",
146
+ "!**/test/**",
147
+ "!**/tests/**",
148
+ "!**/__tests__/**",
149
+ "!**/fixtures/**",
150
+ "!**/testing/**",
151
+ "!**/scripts/**",
152
+ "!**/bench/**",
153
+ "!**/performance/**"
154
+ ]
155
+ }
156
+ ]
157
+ }
@@ -0,0 +1,41 @@
1
+ import { createHash } from "node:crypto"
2
+ import { readFileSync } from "node:fs"
3
+ import { dirname, resolve } from "node:path"
4
+ import { fileURLToPath } from "node:url"
5
+
6
+ const directory = dirname(fileURLToPath(import.meta.url))
7
+ const root = resolve(directory, "../..")
8
+ const manifest = JSON.parse(readFileSync(resolve(directory, "manifest.json"), "utf8"))
9
+ const errors = []
10
+
11
+ for (const [name, expected] of Object.entries(manifest.files)) {
12
+ const actual = createHash("sha256")
13
+ .update(readFileSync(resolve(directory, name)))
14
+ .digest("hex")
15
+ if (actual !== expected) errors.push(`Managed standard changed: ${name}`)
16
+ }
17
+
18
+ const packageJson = JSON.parse(readFileSync(resolve(root, "package.json"), "utf8"))
19
+ for (const [name, expected] of Object.entries(manifest.devDependencies)) {
20
+ if (packageJson.devDependencies?.[name] !== expected) errors.push(`Expected ${name}@${expected}`)
21
+ }
22
+ if (packageJson.packageManager !== manifest.packageManager) errors.push("Package manager differs from shared standard")
23
+
24
+ const biome = JSON.parse(readFileSync(resolve(root, "biome.json"), "utf8"))
25
+ if (!biome.extends?.includes("./config/wirecat/biome.base.json"))
26
+ errors.push("Biome does not extend the shared standard")
27
+ const spell = JSON.parse(readFileSync(resolve(root, "cspell.json"), "utf8"))
28
+ if (!spell.import?.includes("./config/wirecat/cspell.json")) errors.push("CSpell does not import the shared standard")
29
+ const markdown = JSON.parse(readFileSync(resolve(root, ".markdownlint-cli2.jsonc"), "utf8"))
30
+ if (markdown.config?.extends !== "./config/wirecat/markdownlint.json")
31
+ errors.push("Markdown does not extend the shared standard")
32
+ const hooks = readFileSync(resolve(root, "lefthook.yml"), "utf8")
33
+ if (!/^\s*-\s*config\/wirecat\/lefthook\.yml\s*$/m.test(hooks))
34
+ errors.push("Git hooks do not extend the shared standard")
35
+
36
+ if (errors.length) {
37
+ process.stderr.write(`${errors.join("\n")}\nUpdate standards centrally or put project overrides in root configs.\n`)
38
+ process.exitCode = 1
39
+ } else {
40
+ process.stdout.write(`Shared standards match ${manifest.revision}.\n`)
41
+ }
@@ -0,0 +1,80 @@
1
+ {
2
+ "version": "0.2",
3
+ "language": "en,en-GB,ru,es",
4
+ "ignoreRegExpList": [
5
+ "/^\\s*(```|~~~)[\\s\\S]*?^\\s*\\1/gm",
6
+ "/`[^`\\n]*`/g",
7
+ "/<!--[\\s\\S]*?-->/g",
8
+ "/\\]\\([^)]*\\)/g",
9
+ "/<[^>]+>/g",
10
+ "/^---[\\s\\S]*?^---/gm"
11
+ ],
12
+ "words": [
13
+ "armv",
14
+ "brazecli",
15
+ "bunx",
16
+ "claude",
17
+ "codegen",
18
+ "CodeWiki",
19
+ "DeepWiki",
20
+ "Excalidraw",
21
+ "frontmatter",
22
+ "Fumadocs",
23
+ "gitleaks",
24
+ "Hermes",
25
+ "JSONL",
26
+ "keyring",
27
+ "kwallet",
28
+ "leemour",
29
+ "libc",
30
+ "libsecret",
31
+ "Lucene",
32
+ "LUKS",
33
+ "Mermaid",
34
+ "mtcute",
35
+ "npmjs",
36
+ "ONNX",
37
+ "pioh",
38
+ "Repomix",
39
+ "riscv",
40
+ "Shiki",
41
+ "subcommand",
42
+ "tailnet",
43
+ "tailscale",
44
+ "tgcli",
45
+ "unpushed",
46
+ "userbot",
47
+ "volta",
48
+ "webhooks",
49
+ "WireCat",
50
+ "wirecat",
51
+ "автодополнение",
52
+ "бана",
53
+ "вебхук",
54
+ "вебхуки",
55
+ "капчу",
56
+ "контрибьюторы",
57
+ "листателя",
58
+ "Минцифры",
59
+ "опкод",
60
+ "отсканируете",
61
+ "переголосовать",
62
+ "пулл",
63
+ "реквесты",
64
+ "рендерер",
65
+ "спиннера",
66
+ "схлопнет",
67
+ "схлопывает",
68
+ "токенам",
69
+ "токене",
70
+ "токеном",
71
+ "токену",
72
+ "хэша",
73
+ "хэшем"
74
+ ],
75
+ "import": [
76
+ "@cspell/dict-en-gb/cspell-ext.json",
77
+ "@cspell/dict-ru_ru/cspell-ext.json",
78
+ "@cspell/dict-es-es/cspell-ext.json"
79
+ ]
80
+ }
@@ -0,0 +1,26 @@
1
+ # Git hooks. Installed by the `prepare` npm script, which pnpm runs on every install — necessary
2
+ # because pnpm blocks dependencies' build scripts by default, so lefthook's own postinstall never
3
+ # fires and the hooks would otherwise silently never land.
4
+ #
5
+ # Install by hand: pnpm exec lefthook install
6
+ # Skip for one commit: LEFTHOOK=0 git commit …
7
+
8
+ pre-commit:
9
+ parallel: true
10
+ commands:
11
+ biome:
12
+ glob: "*.{ts,tsx,js,mjs,cjs,json,jsonc}"
13
+ # Staged files only. The whole-repo run is the CI gate; running it per-commit is how people
14
+ # end up reaching for --no-verify, which is worse than no hook.
15
+ run: node node_modules/@biomejs/biome/bin/biome check --no-errors-on-unmatched --files-ignore-unknown=true {staged_files}
16
+
17
+ gitleaks:
18
+ # Staged content only, and it warns rather than blocks when gitleaks is not installed: a
19
+ # fresh clone must still be able to commit. CI is the gate that cannot be skipped.
20
+ run: |
21
+ if command -v gitleaks >/dev/null 2>&1; then
22
+ gitleaks git --staged --no-banner --redact .
23
+ else
24
+ echo "gitleaks is not installed — staged changes were NOT scanned for secrets."
25
+ echo " install: https://github.com/gitleaks/gitleaks#installing (CI scans regardless)"
26
+ fi
@@ -0,0 +1,37 @@
1
+ {
2
+ "source": "https://github.com/WireCatLabs/community",
3
+ "revision": "2b0f124f6375939bba10c28a7f091923323e9a13",
4
+ "files": {
5
+ "biome.base.json": "63760337a2e68522ad75e0ba163635785a01ec34dbd3cf338a09a4145932cf2a",
6
+ "cspell.json": "8241122ed4d461976a838099b265ef8f2689a066608257b6fdc250d9b49b58aa",
7
+ "lefthook.yml": "2f65a2e3103006d43f6d6f0a07ab333c132286157763222a59a5a22676acd391",
8
+ "markdownlint.json": "5ed7d6980054c539f3f3c0e47a090f1a725b15be2c486a769a836bc6cd48ad2d",
9
+ "no-deep-wirecat-import.grit": "b9cb69b9ae125604c951cd924941ab089af32f9fa91f072e34531ca861ed125d",
10
+ "no-exec-interpolation.grit": "b9461f887afd0f7d555814bc45c5cd47537808edc10a17b06049f3ca0fff5957",
11
+ "no-new-function.grit": "f6423e186cca90b1ab87d40185c6d920e99d83366af6dc11455963a0797fb627",
12
+ "no-shell-true.grit": "d3943cfa1d7ebbf7be0fa22c2aaf2b7d7aa9ff462f2eb14a7798632764e3875c",
13
+ "no-unbounded-spread.grit": "f4178e5b74212fef58fec8916b6783d9f4f192ac618aa4ced041c4167bfbf81b",
14
+ "release-lock-info.mjs": "4abab1f01ceee7d4c709222e923c9a6fc634bcf7b8b64d745a9ac495993310e0",
15
+ "release-lock.sh": "bef364092c4ab595f3b9b4130e23570c8306223228cdfa3e9384230f3b7f4a49",
16
+ "rumdl.toml": "048b929176f13234324f5825d1e811397f1491b9cb6684a1953e268e2c2ed863",
17
+ "check.mjs": "34b266411eaab7707030915f645308e86784b1574b21fa44ca974861a90c9ce1",
18
+ "no-sqlite-driver.grit": "19e1f510a88ddd9df44d6db43c1c9b603c3e5bbec03a3f65305b72eac7f09389"
19
+ },
20
+ "packageManager": "pnpm@11.20.0",
21
+ "node": ">=22",
22
+ "devDependencies": {
23
+ "@biomejs/biome": "2.5.15",
24
+ "@cspell/dict-en-gb": "5.0.34",
25
+ "@cspell/dict-es-es": "3.0.9",
26
+ "@cspell/dict-ru_ru": "2.3.3",
27
+ "cspell": "10.3.6",
28
+ "lefthook": "2.1.16",
29
+ "markdownlint-cli2": "0.23.3"
30
+ },
31
+ "fileRevisions": {
32
+ "release-lock.sh": "49e630a99d758e3e113749bef2049b258c7262e3",
33
+ "release-lock-info.mjs": "49e630a99d758e3e113749bef2049b258c7262e3",
34
+ "biome.base.json": "06c612c609d870f0ba00010612e0413f65340408",
35
+ "no-sqlite-driver.grit": "06c612c609d870f0ba00010612e0413f65340408"
36
+ }
37
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "default": false,
3
+ "MD001": false,
4
+ "MD010": true,
5
+ "MD012": {
6
+ "maximum": 2
7
+ },
8
+ "MD040": true,
9
+ "MD042": true
10
+ }
@@ -0,0 +1,15 @@
1
+ language js
2
+
3
+ or {
4
+ `import $_ from $source`,
5
+ `import $source`,
6
+ `export { $_ } from $source`,
7
+ `export * from $source`,
8
+ `export * as $_ from $source`,
9
+ `export type { $_ } from $source`,
10
+ `import($source)`,
11
+ `require($source)`
12
+ } where {
13
+ $source <: r"^[\"'`]@wirecat/[^/\"'`]+/(?:src|dist)(?:/.*)?[\"'`]$",
14
+ register_diagnostic(span=$source, message="Import WireCat packages through their public exports, never src/ or dist/: those paths are not part of the package contract and break on the next release.", severity="error")
15
+ }
@@ -0,0 +1,17 @@
1
+ language js
2
+
3
+ or {
4
+ `exec($args)`,
5
+ `execSync($args)`,
6
+ `$_.execSync($args)`,
7
+ `child_process.exec($args)`,
8
+ `childProcess.exec($args)`,
9
+ `cp.exec($args)`
10
+ } where {
11
+ $args <: [$command, ...],
12
+ $command <: or {
13
+ r"^`[\s\S]*\$\{[\s\S]*`$",
14
+ `$left + $right`
15
+ },
16
+ register_diagnostic(span=$command, message="A command string built from values runs through a shell. Use execFile or spawn with an argument array.", severity="error")
17
+ }
@@ -0,0 +1,8 @@
1
+ language js
2
+
3
+ or {
4
+ `new Function($args)`,
5
+ `Function($args)`
6
+ } as $call where {
7
+ register_diagnostic(span=$call, message="Function() compiles a string into code, like eval. Pass a function value instead.", severity="error")
8
+ }
@@ -0,0 +1,8 @@
1
+ language js
2
+
3
+ `shell: true` as $option where {
4
+ $option <: within `$callee($args)` where {
5
+ $callee <: r"^(?:.*\.)?(?:spawn|spawnSync|exec|execSync|execFile|execFileSync|fork)$"
6
+ },
7
+ register_diagnostic(span=$option, message="shell: true hands the arguments to a shell, so a space, quote or ; in user data runs as a command. Call the program directly with an argument array.", severity="error")
8
+ }
@@ -0,0 +1,15 @@
1
+ language js
2
+
3
+ or {
4
+ `import $_ from $source`,
5
+ `import $source`,
6
+ `export { $_ } from $source`,
7
+ `export * from $source`,
8
+ `export * as $_ from $source`,
9
+ `export type { $_ } from $source`,
10
+ `import($source)`,
11
+ `require($source)`
12
+ } where {
13
+ $source <: r"^[\"'`](?:node:sqlite|bun:sqlite|better-sqlite3|drizzle-orm)(?:/.*)?[\"'`]$",
14
+ register_diagnostic(span=$source, message="The shared store belongs to cli-core (WC-STORE-5): a CLI opens no SQLite driver and writes no SQL against it. Ask cli-core for a store method.", severity="error")
15
+ }
@@ -0,0 +1,10 @@
1
+ language js
2
+
3
+ or {
4
+ `$target.push($args)`,
5
+ `Math.max($args)`,
6
+ `Math.min($args)`
7
+ } as $call where {
8
+ $args <: some `...$items`,
9
+ register_diagnostic(span=$call, message="Spreading a list into call arguments throws past about 125,000 items. Loop or reduce instead.", severity="warn")
10
+ }
@@ -0,0 +1,71 @@
1
+ import { execFileSync } from "node:child_process"
2
+ import { closeSync, openSync, readSync, realpathSync } from "node:fs"
3
+ import { fileURLToPath } from "node:url"
4
+
5
+ const safe = (value) => JSON.stringify(String(value))
6
+
7
+ export function releaseLockInfo(path, { run = execFileSync, realpath = realpathSync, label = readLabel } = {}) {
8
+ const lines = [`another release holds ${safe(path)}`]
9
+ let rows
10
+ for (const columns of ["PID,COMMAND,PATH,TYPE,MODE,HOLDERS", "PID,COMMAND,PATH,TYPE,MODE"]) {
11
+ try {
12
+ const result = JSON.parse(
13
+ run("lslocks", ["--json", "--output", columns], {
14
+ encoding: "utf8",
15
+ timeout: 2000,
16
+ maxBuffer: 1024 * 1024,
17
+ stdio: ["ignore", "pipe", "ignore"],
18
+ }),
19
+ )
20
+ rows = result.locks
21
+ break
22
+ } catch {
23
+ // Older util-linux lacks HOLDERS; diagnostics also work without lslocks.
24
+ }
25
+ }
26
+ const holders = new Set()
27
+ if (Array.isArray(rows)) {
28
+ for (const row of rows) {
29
+ try {
30
+ if (row.type !== "FLOCK" || row.mode?.endsWith("*")) continue
31
+ if (realpath(row.path) !== realpath(path)) continue
32
+ const before = holders.size
33
+ if (Array.isArray(row.holders)) {
34
+ for (const holder of row.holders) {
35
+ const match = /^(\d+),([^,]+),\d+$/.exec(holder)
36
+ if (match && Number(match[1]) > 0) holders.add(`pid ${match[1]} (${safe(match[2])})`)
37
+ }
38
+ }
39
+ if (holders.size === before && Number.isInteger(row.pid) && row.pid > 0)
40
+ holders.add(`pid ${row.pid} (${safe(row.command)})`)
41
+ } catch {
42
+ // A holder may exit while the snapshot is read, or hide its path.
43
+ }
44
+ }
45
+ }
46
+ lines.push(
47
+ holders.size
48
+ ? `Observed lock holders (may change): ${[...holders].join(", ")}`
49
+ : "Observed lock holders: unavailable",
50
+ )
51
+ try {
52
+ const recorded = label(`${path}.who`)
53
+ if (recorded) lines.push(`Last recorded release (may be stale): ${safe(recorded)}`)
54
+ } catch {
55
+ // The label is advisory; only flock decides whether a release may proceed.
56
+ }
57
+ lines.push("Wait for the lock holders to finish, then retry. Do not remove the lock file.")
58
+ return `${lines.join("\n")}\n`
59
+ }
60
+
61
+ function readLabel(path) {
62
+ const fd = openSync(path, "r")
63
+ try {
64
+ const bytes = Buffer.alloc(512)
65
+ return bytes.subarray(0, readSync(fd, bytes)).toString("utf8").trim()
66
+ } finally {
67
+ closeSync(fd)
68
+ }
69
+ }
70
+
71
+ if (process.argv[1] === fileURLToPath(import.meta.url)) process.stderr.write(releaseLockInfo(process.argv[2]))
@@ -0,0 +1,34 @@
1
+ # Source from the repository root; FD9 must stay in the release shell and its children.
2
+ wirecat_release_lock() {
3
+ _wirecat_release_wait=${1-0}
4
+ case "$_wirecat_release_wait" in
5
+ ''|*[!0-9]*) echo "release wait must be a nonnegative integer" >&2; return 2 ;;
6
+ esac
7
+ command -v flock >/dev/null 2>&1 || {
8
+ echo "release locking requires flock" >&2
9
+ return 1
10
+ }
11
+ # Keyed by package name, not folder: one package releases from several checkouts and worktrees.
12
+ _wirecat_release_key=$(node -e '
13
+ const { name } = JSON.parse(require("node:fs").readFileSync("package.json", "utf8"))
14
+ if (typeof name !== "string" || !name) throw Error("package.json has no name")
15
+ process.stdout.write(name.replace(/^@/, "").replace(/[^A-Za-z0-9._-]+/g, "-"))
16
+ ' 2>/dev/null) && [ -n "$_wirecat_release_key" ] || {
17
+ echo "release locking needs a package name in ./package.json" >&2
18
+ return 1
19
+ }
20
+ _wirecat_release_lock="${XDG_RUNTIME_DIR:-/tmp}/leemour-release-$_wirecat_release_key.lock"
21
+ exec 9>>"$_wirecat_release_lock"
22
+ if flock -E 75 -w "$_wirecat_release_wait" 9; then
23
+ printf '%s pid %s since %s\n' "$(basename "$PWD")" "$$" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$_wirecat_release_lock.who"
24
+ else
25
+ _wirecat_release_status=$?
26
+ if [ "$_wirecat_release_status" -ne 75 ]; then
27
+ printf 'could not acquire release lock %s (flock exit %s)\n' "$_wirecat_release_lock" "$_wirecat_release_status" >&2
28
+ return 1
29
+ fi
30
+ node ./config/wirecat/release-lock-info.mjs "$_wirecat_release_lock" ||
31
+ printf 'another release holds %s; holder details unavailable\n' "$_wirecat_release_lock" >&2
32
+ return 1
33
+ fi
34
+ }
@@ -0,0 +1,9 @@
1
+ # MD013 (line length): prose is wrapped by hand at 100–120 and tables cannot wrap.
2
+ # MD041 (first line a heading): READMEs open with the logo; docs-check --pages checks the pages.
3
+ # MD057 (relative links): docs-check follows every link and anchor already.
4
+ [global]
5
+ disable = ["MD013", "MD041", "MD057"]
6
+ exclude = ["docs/commands.md", "node_modules", "dist", "docs_ai"]
7
+
8
+ [MD033]
9
+ allowed-elements = ["p", "img", "a", "br", "details", "summary", "sup", "sub", "kbd"]
@@ -0,0 +1,16 @@
1
+ app — a new WireCat CLI, started from cli-template
2
+
3
+ Usage: app <profile> <command> [options]
4
+ Account commands start with your profile, a name you choose; none is ever assumed.
5
+
6
+ Works with:
7
+ tg Telegram in the same local store: search, review and tasks offline
8
+ max MAX in the same local store: search, review and tasks offline
9
+ wa WhatsApp in the same local store: search, review and tasks offline
10
+ zm Zoom meetings: transcripts and summaries, searchable with your messages
11
+ drive Google Drive and Docs as local text, searchable with your messages
12
+ memo context <person> one person across messengers, mail, notes and meetings
13
+
14
+ Next: app commands every command, by section
15
+ app help options output, offline, timeouts, dry run
16
+ For agents: app skill show rules and the discovery loop