axstack 0.34.0 → 0.34.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -147,6 +147,24 @@ Eligible own PRs use guarded automatic merge; see [merge boundaries, exclusions,
147
147
  See [PR scope and sizing](skills/axstack/references/pr-shape.md) for candidate
148
148
  shape and [Releases](https://github.com/axatbhardwaj/axstack/releases) for release history.
149
149
 
150
+ ## Development
151
+
152
+ ```sh
153
+ bun install --frozen-lockfile
154
+ bun run lint
155
+ bun run typecheck
156
+ git config core.hooksPath .husky
157
+ ```
158
+
159
+ The hook is opt-in per clone via `git config core.hooksPath .husky` and requires
160
+ `bun install`. Without dev tools it prints a setup hint and skips the checks.
161
+ The hook runs lint and typecheck only. Biome formatting is optional for editors
162
+ or manual use; commits do not enforce formatting. TypeScript checks JavaScript
163
+ in `src/` and `bin/` without emitting files. All tooling is development-only and
164
+ is excluded from packed assets by `package.json.files`.
165
+ Regenerate `bun.lock` with the minimum Bun version in `package.json.engines`
166
+ (currently 1.3.14); newer Bun versions can write an incompatible lockfile format.
167
+
150
168
  ## License
151
169
 
152
170
  [MIT](LICENSE) © 2026 Axat Bhardwaj.
package/bin/axstack.js CHANGED
@@ -26,7 +26,7 @@ function homeDir() {
26
26
  // Tilde expansion needs a known absolute home; with HOME missing, empty,
27
27
  // or relative the destination is unknown, so refuse even with --yes.
28
28
  const home = Bun.env.HOME;
29
- if (!home || !home.startsWith('/')) {
29
+ if (!home?.startsWith('/')) {
30
30
  throw new Error('HOME is missing, empty, or not absolute; pass explicit paths instead of ~');
31
31
  }
32
32
  return home;
@@ -298,6 +298,13 @@ async function main() {
298
298
  const legacyCodexManifest = legacyCodexDir
299
299
  ? await readLegacySkillsManifest(legacyCodexDir)
300
300
  : null;
301
+ /**
302
+ * @type {Awaited<ReturnType<typeof installBundle>> & {
303
+ * legacyCodex?: {removed: string[], preserved: string[], missing: string[], skipped?: boolean,
304
+ * held?: boolean, failed?: boolean, failure?: boolean, reason?: string},
305
+ * addedRoleIds?: string[], removedRoleIds?: string[], changedRoleModels?: string[], notes?: string[]
306
+ * }}
307
+ */
301
308
  const summary = await installBundle({
302
309
  bundleDir: flags.bundle ? resolve(flags.bundle) : PACKAGE_ROOT,
303
310
  skillsDir,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "axstack",
3
- "version": "0.34.0",
3
+ "version": "0.34.1",
4
4
  "description": "Axstack installer and setup CLI: installs owned chat skills and role data, configures supported harness settings, and checks T3 Code capabilities.",
5
5
  "keywords": [
6
6
  "claude-code",
@@ -32,8 +32,15 @@
32
32
  "docs/*.md"
33
33
  ],
34
34
  "scripts": {
35
+ "lint": "biome lint . --error-on-warnings",
36
+ "typecheck": "bun node_modules/typescript/bin/tsc --noEmit --project jsconfig.json",
35
37
  "test": "bun test",
36
38
  "test:all": "bun test"
37
39
  },
38
- "license": "MIT"
40
+ "license": "MIT",
41
+ "devDependencies": {
42
+ "@biomejs/biome": "2.5.15",
43
+ "@types/bun": "1.4.2",
44
+ "typescript": "7.0.2"
45
+ }
39
46
  }
@@ -54,7 +54,7 @@ function fetchCurrent(args) {
54
54
  }
55
55
 
56
56
  function nodes(connection, field) {
57
- if (!connection || connection.pageInfo?.hasNextPage !== false || !Array.isArray(connection.nodes)) {
57
+ if (connection?.pageInfo?.hasNextPage !== false || !Array.isArray(connection.nodes)) {
58
58
  throw new Error(`incomplete ${field} connection`);
59
59
  }
60
60
  return connection.nodes;
package/src/archify.js CHANGED
@@ -19,6 +19,7 @@ export function archifyGit(path, ...args) {
19
19
 
20
20
  function verifySha(record, actual) {
21
21
  if (actual === record.sha) return;
22
+ /** @type {Error & {code?: string}} */
22
23
  const error = new Error(`archify SHA mismatch: expected ${record.sha}, got ${actual}`);
23
24
  error.code = 'ARCHIFY_SHA_MISMATCH';
24
25
  throw error;
@@ -31,7 +32,8 @@ async function ownersSnapshot(path) {
31
32
  });
32
33
  if (st && (!st.isFile() || st.isSymbolicLink())) throw new Error(`unsafe archify owners file: ${path}`);
33
34
  const bytes = st ? await readFile(path) : null;
34
- const owners = bytes ? JSON.parse(bytes) : [];
35
+ // JSON.parse coerces its argument to a string; preserve the existing Buffer coercion.
36
+ const owners = bytes ? /** @type {(text: {toString(): string}) => unknown} */ (JSON.parse)(bytes) : [];
35
37
  if (!Array.isArray(owners) || owners.some((owner) => typeof owner !== 'string' || !owner.startsWith('/'))) {
36
38
  throw new Error(`invalid archify owners file: ${path}`);
37
39
  }
@@ -184,7 +184,7 @@ export async function planUninstallClaudeSettings({ claude, skillsRoot, boundPat
184
184
  }
185
185
 
186
186
  const owned = sidecar.keys[CLAUDE_SETTING_PATH];
187
- if (!owned || !owned.owners.includes(skillsRoot)) {
187
+ if (!owned?.owners.includes(skillsRoot)) {
188
188
  return {
189
189
  report: { status: 'skipped', reason: 'this skills directory does not own the Claude setting' },
190
190
  writes: [],
package/src/installer.js CHANGED
@@ -179,7 +179,7 @@ function homeDir() {
179
179
  // aliased HOME) compare equal to canonicalized targets; an unresolvable
180
180
  // HOME also yields null and fails closed rather than bypassing the guard.
181
181
  const home = Bun.env.HOME;
182
- if (!home || !home.startsWith('/')) return null;
182
+ if (!home?.startsWith('/')) return null;
183
183
  try {
184
184
  return realpathSync(home);
185
185
  } catch {
@@ -238,7 +238,7 @@ export async function validateBundle(bundleDir, selectedPreset = null) {
238
238
  if (skillsStat.isSymbolicLink() || !skillsStat.isDirectory()) {
239
239
  throw new Error('bundle skills/ must be a real directory, not a symlink');
240
240
  }
241
- let entries = await readdir(skillsRoot, { withFileTypes: true });
241
+ const entries = await readdir(skillsRoot, { withFileTypes: true });
242
242
  // Symlinked (or non-directory) top-level skill entries must be rejected,
243
243
  // not silently skipped: Dirent.isDirectory() is false for symlinks.
244
244
  for (const entry of entries) {
@@ -380,6 +380,11 @@ async function walkSkills(dir, skillsRoot, out) {
380
380
  // never followed or truncated. After the rename, existing permissions are
381
381
  // preserved; new files take an explicit `mode` (e.g. 0600 for configs) or
382
382
  // the umask default. Guards pre-existing entries, not active races.
383
+ /**
384
+ * @param {string} dest
385
+ * @param {string | Uint8Array} bytes
386
+ * @param {{mode?: number}} [options]
387
+ */
383
388
  export async function writeAtomic(dest, bytes, { mode } = {}) {
384
389
  await mkdir(dirname(dest), { recursive: true });
385
390
  let existingMode = null;
@@ -408,6 +413,7 @@ export async function writeAtomic(dest, bytes, { mode } = {}) {
408
413
  }
409
414
  }
410
415
 
416
+ /** @param {Pick<InstallerOptions, 'skillsDir' | 'instructionsPath'>} [options] */
411
417
  export async function checkInstructionBinding({ skillsDir, instructionsPath } = {}) {
412
418
  if (!skillsDir) throw new Error('instruction check requires --skills-dir <dir> or --harness <name>');
413
419
  if (!instructionsPath) throw new Error('instruction check requires --instructions <file> or a supported harness');
@@ -467,6 +473,21 @@ async function assertFileSnapshot(dest, expected) {
467
473
  }
468
474
  }
469
475
 
476
+ /**
477
+ * @typedef {{available: boolean, settingsPath?: string, skip?: boolean, reason?: string}} ClaudeOption
478
+ * @typedef {Object} InstallerOptions
479
+ * @property {string} [skillsDir]
480
+ * @property {string} [instructionsPath]
481
+ * @property {boolean} [force]
482
+ * @property {boolean} [yes]
483
+ * @property {ClaudeOption} [claude]
484
+ * @property {(message: string) => void} [log]
485
+ */
486
+
487
+ /**
488
+ * @param {InstallerOptions & {bundleDir?: string, preset?: string, toolsDir?: string,
489
+ * inheritedInstructions?: {path: string, hash: string}}} [options]
490
+ */
470
491
  export async function installBundle({
471
492
  bundleDir,
472
493
  skillsDir,
@@ -857,6 +878,7 @@ export async function installBundle({
857
878
  }
858
879
  }
859
880
 
881
+ /** @param {InstallerOptions} [options] */
860
882
  export async function uninstallBundle({
861
883
  skillsDir,
862
884
  instructionsPath = null,
@@ -1110,6 +1132,10 @@ export async function uninstallBundle({
1110
1132
  // is present and byte-for-byte verified. This deliberately handles skill
1111
1133
  // payloads only: instruction files, Claude settings, and historical profile
1112
1134
  // ownership may belong to another harness and remain bound to the old manifest.
1135
+ /**
1136
+ * @param {{canonicalSkillsDir?: string, legacySkillsDir?: string, yes?: boolean,
1137
+ * acceptedInstructionTransfer?: {path: string, legacyHash: string, canonicalHash: string}}} [options]
1138
+ */
1113
1139
  export async function retireLegacySkills({
1114
1140
  canonicalSkillsDir,
1115
1141
  legacySkillsDir,
package/src/posixpath.js CHANGED
@@ -65,7 +65,8 @@ export function resolveFrom(cwd, ...paths) {
65
65
  }
66
66
 
67
67
  export function resolve(...paths) {
68
- return resolveFrom(Bun.cwd, ...paths);
68
+ // Bun.cwd is a runtime string missing from the current Bun declarations.
69
+ return resolveFrom(/** @type {typeof Bun & {cwd: string}} */ (Bun).cwd, ...paths);
69
70
  }
70
71
 
71
72
  export function relative(from, to) {