@alavida/agentpack 0.1.1 → 0.1.2

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
@@ -251,7 +251,11 @@ This package also ships an agent-facing skill under:
251
251
  node_modules/@alavida/agentpack/skills/agentpack-cli/SKILL.md
252
252
  ```
253
253
 
254
- If your repo uses TanStack Intent, you can map that shipped skill into your agent workflow so the agent knows how to use `agentpack` correctly inside downstream repos. This is optional. It is not required to use the CLI.
254
+ Consumers can use TanStack Intent in their repos to map that shipped skill into their agent workflow so the agent knows how to use `agentpack` correctly.
255
+
256
+ This is recommended if you want downstream coding agents to follow the right `agentpack` lifecycle automatically.
257
+
258
+ It is still optional. You do not need Intent to install or run the CLI itself.
255
259
 
256
260
  ## Metadata Policy
257
261
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alavida/agentpack",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Package-backed skills lifecycle CLI for agent skills and plugins",
5
5
  "type": "module",
6
6
  "bin": {
@@ -62,8 +62,15 @@ Key idea:
62
62
  - `SKILL.md.requires` is the source of truth
63
63
  - `package.json.dependencies` is the compiled mirror
64
64
  - `validate` and `dev` sync dependencies automatically
65
+ - `skills validate` records the current source-hash snapshot in `.agentpack/build-state.json`
65
66
  - `skills dev` materializes the compiled skill artifact for runtime use
66
67
 
68
+ Persistence rule:
69
+
70
+ - commit `.agentpack/build-state.json` so stale detection works across GitHub, CI, and teammate machines
71
+ - commit `.agentpack/catalog.json` in authoring repos
72
+ - do not commit `.agentpack/install.json`
73
+
67
74
  Runtime notes:
68
75
 
69
76
  - after `skills dev` writes to `.claude/skills/` or `.agents/skills/`, start a fresh agent session if the current one was already running
@@ -112,6 +119,11 @@ Default flow:
112
119
  - `agentpack skills stale <skill>`
113
120
  - `agentpack skills validate <skill>`
114
121
 
122
+ Key idea:
123
+
124
+ - `skills stale` compares current source hashes to the last validated snapshot in `.agentpack/build-state.json`
125
+ - if that file is not committed, stale detection will not persist across clones or CI runs
126
+
115
127
  ## Conceptual Frame
116
128
 
117
129
  When the user is reasoning about the model itself, explain agentpack this way:
@@ -42,9 +42,15 @@ Use this when the user is creating or changing a packaged skill in the same repo
42
42
  What each step means:
43
43
 
44
44
  - `inspect` explains what the skill currently is
45
- - `validate` checks package readiness and source existence
45
+ - `validate` checks package readiness, source existence, and records the validated source snapshot in `.agentpack/build-state.json`
46
46
  - `dev` links the skill into `.claude/skills/` and `.agents/skills/` for local testing
47
47
 
48
+ Important persistence behavior:
49
+
50
+ - commit `.agentpack/build-state.json` if you want `skills stale` to work across GitHub, CI, and teammate machines
51
+ - commit `.agentpack/catalog.json` in authoring repos
52
+ - do not commit `.agentpack/install.json`
53
+
48
54
  Important runtime behavior:
49
55
 
50
56
  - if the agent session was already running before `skills dev`, start a fresh session so the runtime can rescan the linked skill
@@ -76,3 +82,17 @@ Consumption stage:
76
82
  - `skills env`
77
83
 
78
84
  Do not substitute one for the other.
85
+
86
+ ## Stale Detection Contract
87
+
88
+ `skills stale` is not comparing against memory or local runtime state.
89
+
90
+ It compares current source hashes against the last validated snapshot recorded in `.agentpack/build-state.json`.
91
+
92
+ That means:
93
+
94
+ 1. run `agentpack skills validate <skill-dir>`
95
+ 2. commit the updated `.agentpack/build-state.json`
96
+ 3. later source changes can be detected by `agentpack skills stale`
97
+
98
+ If the build-state file is not committed, stale detection will only work on the machine where validation was last run.
@@ -0,0 +1,5 @@
1
+ import { buildPlugin } from '../../lib/plugins.js';
2
+
3
+ export function buildPluginUseCase(target, options) {
4
+ return buildPlugin(target, options);
5
+ }
@@ -0,0 +1,5 @@
1
+ import { inspectPluginBundle } from '../../lib/plugins.js';
2
+
3
+ export function inspectPluginBundleUseCase(target, options) {
4
+ return inspectPluginBundle(target, options);
5
+ }
@@ -0,0 +1,5 @@
1
+ import { validatePluginBundle } from '../../lib/plugins.js';
2
+
3
+ export function validatePluginBundleUseCase(target, options) {
4
+ return validatePluginBundle(target, options);
5
+ }
@@ -0,0 +1,5 @@
1
+ import { inspectSkill } from '../../lib/skills.js';
2
+
3
+ export function inspectSkillUseCase(target, options) {
4
+ return inspectSkill(target, options);
5
+ }
@@ -0,0 +1,9 @@
1
+ import { inspectStaleSkill, listStaleSkills } from '../../lib/skills.js';
2
+
3
+ export function listStaleSkillsUseCase(options) {
4
+ return listStaleSkills(options);
5
+ }
6
+
7
+ export function inspectStaleSkillUseCase(target, options) {
8
+ return inspectStaleSkill(target, options);
9
+ }
@@ -0,0 +1,5 @@
1
+ import { validateSkills } from '../../lib/skills.js';
2
+
3
+ export function validateSkillsUseCase(target, options) {
4
+ return validateSkills(target, options);
5
+ }
@@ -1,5 +1,8 @@
1
1
  import { Command } from 'commander';
2
- import { buildPlugin, inspectPluginBundle, startPluginDev, validatePluginBundle } from '../lib/plugins.js';
2
+ import { buildPluginUseCase } from '../application/plugins/build-plugin.js';
3
+ import { inspectPluginBundleUseCase } from '../application/plugins/inspect-plugin-bundle.js';
4
+ import { validatePluginBundleUseCase } from '../application/plugins/validate-plugin-bundle.js';
5
+ import { startPluginDev } from '../lib/plugins.js';
3
6
  import { output } from '../utils/output.js';
4
7
  import { EXIT_CODES } from '../utils/errors.js';
5
8
 
@@ -13,7 +16,7 @@ export function pluginCommand() {
13
16
  .argument('<target>', 'Plugin directory path')
14
17
  .action((target, opts, command) => {
15
18
  const globalOpts = command.optsWithGlobals();
16
- const result = inspectPluginBundle(target);
19
+ const result = inspectPluginBundleUseCase(target);
17
20
 
18
21
  if (globalOpts.json) {
19
22
  output.json(result);
@@ -76,7 +79,7 @@ export function pluginCommand() {
76
79
  .argument('<target>', 'Plugin directory path')
77
80
  .action((target, opts, command) => {
78
81
  const globalOpts = command.optsWithGlobals();
79
- const result = validatePluginBundle(target);
82
+ const result = validatePluginBundleUseCase(target);
80
83
 
81
84
  if (globalOpts.json) {
82
85
  output.json(result);
@@ -114,7 +117,7 @@ export function pluginCommand() {
114
117
  .argument('<target>', 'Plugin directory path')
115
118
  .action((target, opts, command) => {
116
119
  const globalOpts = command.optsWithGlobals();
117
- const result = buildPlugin(target, { clean: opts.clean });
120
+ const result = buildPluginUseCase(target, { clean: opts.clean });
118
121
 
119
122
  if (globalOpts.json) {
120
123
  output.json(result);
@@ -1,20 +1,19 @@
1
1
  import { Command } from 'commander';
2
+ import { inspectSkillUseCase } from '../application/skills/inspect-skill.js';
3
+ import { inspectStaleSkillUseCase, listStaleSkillsUseCase } from '../application/skills/list-stale-skills.js';
4
+ import { validateSkillsUseCase } from '../application/skills/validate-skills.js';
2
5
  import {
3
6
  inspectSkillDependencies,
4
7
  inspectMissingSkillDependencies,
5
8
  inspectSkillsStatus,
6
9
  inspectRegistryConfig,
7
- inspectSkill,
8
10
  inspectSkillsEnv,
9
- inspectStaleSkill,
10
11
  installSkills,
11
12
  listOutdatedSkills,
12
- listStaleSkills,
13
13
  resolveInstallTargets,
14
14
  startSkillDev,
15
15
  unlinkSkill,
16
16
  uninstallSkills,
17
- validateSkills,
18
17
  } from '../lib/skills.js';
19
18
  import { output } from '../utils/output.js';
20
19
  import { EXIT_CODES } from '../utils/errors.js';
@@ -250,7 +249,7 @@ export function skillsCommand() {
250
249
  .argument('<target>', 'Skill directory, SKILL.md path, or package name')
251
250
  .action((target, opts, command) => {
252
251
  const globalOpts = command.optsWithGlobals();
253
- const result = inspectSkill(target);
252
+ const result = inspectSkillUseCase(target);
254
253
 
255
254
  if (globalOpts.json) {
256
255
  output.json(result);
@@ -291,7 +290,7 @@ export function skillsCommand() {
291
290
  const globalOpts = command.optsWithGlobals();
292
291
 
293
292
  if (target) {
294
- const result = inspectStaleSkill(target);
293
+ const result = inspectStaleSkillUseCase(target);
295
294
 
296
295
  if (globalOpts.json) {
297
296
  output.json(result);
@@ -310,7 +309,7 @@ export function skillsCommand() {
310
309
  return;
311
310
  }
312
311
 
313
- const results = listStaleSkills();
312
+ const results = listStaleSkillsUseCase();
314
313
 
315
314
  if (globalOpts.json) {
316
315
  output.json({
@@ -337,7 +336,7 @@ export function skillsCommand() {
337
336
  .argument('[target]', 'Optional packaged skill directory, SKILL.md path, or package name')
338
337
  .action((target, opts, command) => {
339
338
  const globalOpts = command.optsWithGlobals();
340
- const result = validateSkills(target);
339
+ const result = validateSkillsUseCase(target);
341
340
 
342
341
  if (globalOpts.json) {
343
342
  output.json(
@@ -0,0 +1,136 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+
4
+ export function readSkillGraphNode(repoRoot, packageDir, {
5
+ directInstallNames = new Set(),
6
+ parseSkillFrontmatterFile,
7
+ readPackageMetadata,
8
+ findPackageDirByName,
9
+ normalizeDisplayPath,
10
+ } = {}) {
11
+ const skillFile = join(packageDir, 'SKILL.md');
12
+ if (!existsSync(skillFile)) return null;
13
+
14
+ const skillMetadata = parseSkillFrontmatterFile(skillFile);
15
+ const packageMetadata = readPackageMetadata(packageDir);
16
+ if (!packageMetadata.packageName) return null;
17
+
18
+ const dependencyNames = Object.keys(packageMetadata.dependencies || {})
19
+ .filter((dependencyName) => {
20
+ const localPackageDir = findPackageDirByName(repoRoot, dependencyName);
21
+ if (localPackageDir && existsSync(join(localPackageDir, 'SKILL.md'))) return true;
22
+ const installedPackageDir = join(repoRoot, 'node_modules', ...dependencyName.split('/'));
23
+ return existsSync(join(installedPackageDir, 'SKILL.md'));
24
+ })
25
+ .sort((a, b) => a.localeCompare(b));
26
+
27
+ return {
28
+ name: skillMetadata.name,
29
+ packageName: packageMetadata.packageName,
30
+ packageVersion: packageMetadata.packageVersion,
31
+ skillPath: normalizeDisplayPath(repoRoot, packageDir),
32
+ skillFile: normalizeDisplayPath(repoRoot, skillFile),
33
+ direct: directInstallNames.has(packageMetadata.packageName),
34
+ dependencies: dependencyNames,
35
+ };
36
+ }
37
+
38
+ export function buildSkillGraph(repoRoot, packageDirs, options) {
39
+ const nodes = new Map();
40
+
41
+ for (const packageDir of packageDirs) {
42
+ const node = readSkillGraphNode(repoRoot, packageDir, options);
43
+ if (!node) continue;
44
+ nodes.set(node.packageName, node);
45
+ }
46
+
47
+ return nodes;
48
+ }
49
+
50
+ export function buildReverseDependencies(nodes) {
51
+ const reverse = new Map();
52
+ for (const packageName of nodes.keys()) reverse.set(packageName, []);
53
+
54
+ for (const node of nodes.values()) {
55
+ for (const dependencyName of node.dependencies || []) {
56
+ if (!reverse.has(dependencyName)) continue;
57
+ reverse.get(dependencyName).push(node.packageName);
58
+ }
59
+ }
60
+
61
+ for (const values of reverse.values()) values.sort((a, b) => a.localeCompare(b));
62
+ return reverse;
63
+ }
64
+
65
+ export function buildSkillStatusMap(nodes, staleSkills = new Set()) {
66
+ const cache = new Map();
67
+
68
+ function resolveStatus(packageName, seen = new Set()) {
69
+ if (cache.has(packageName)) return cache.get(packageName);
70
+ if (staleSkills.has(packageName)) {
71
+ cache.set(packageName, 'stale');
72
+ return 'stale';
73
+ }
74
+
75
+ if (seen.has(packageName)) return 'current';
76
+ seen.add(packageName);
77
+
78
+ const node = nodes.get(packageName);
79
+ if (!node) {
80
+ cache.set(packageName, null);
81
+ return null;
82
+ }
83
+
84
+ const dependencyStatuses = (node.dependencies || [])
85
+ .map((dependencyName) => resolveStatus(dependencyName, new Set(seen)))
86
+ .filter(Boolean);
87
+
88
+ const status = dependencyStatuses.some((value) => value === 'stale' || value === 'affected')
89
+ ? 'affected'
90
+ : 'current';
91
+
92
+ cache.set(packageName, status);
93
+ return status;
94
+ }
95
+
96
+ for (const packageName of nodes.keys()) {
97
+ resolveStatus(packageName);
98
+ }
99
+
100
+ return cache;
101
+ }
102
+
103
+ export function readNodeStatus(statusMap, packageName) {
104
+ if (!statusMap) return null;
105
+ return statusMap.get(packageName) || null;
106
+ }
107
+
108
+ export function resolveDependencyClosure(initialRequires, { resolveNode }) {
109
+ const seen = new Set();
110
+ const queue = [...initialRequires];
111
+ const resolved = [];
112
+ const unresolved = [];
113
+
114
+ while (queue.length > 0) {
115
+ const packageName = queue.shift();
116
+ if (seen.has(packageName)) continue;
117
+ seen.add(packageName);
118
+
119
+ const node = resolveNode(packageName);
120
+ if (!node) {
121
+ unresolved.push(packageName);
122
+ continue;
123
+ }
124
+
125
+ resolved.push(node);
126
+
127
+ for (const requirement of node.requires || []) {
128
+ if (!seen.has(requirement)) queue.push(requirement);
129
+ }
130
+ }
131
+
132
+ resolved.sort((a, b) => a.packageName.localeCompare(b.packageName));
133
+ unresolved.sort((a, b) => a.localeCompare(b));
134
+
135
+ return { resolved, unresolved };
136
+ }
@@ -0,0 +1,187 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { join, relative } from 'node:path';
3
+ import { NotFoundError, ValidationError } from '../../utils/errors.js';
4
+
5
+ function parseScalar(value) {
6
+ const trimmed = value.trim();
7
+ if (
8
+ (trimmed.startsWith('"') && trimmed.endsWith('"')) ||
9
+ (trimmed.startsWith("'") && trimmed.endsWith("'"))
10
+ ) {
11
+ return trimmed.slice(1, -1);
12
+ }
13
+ return trimmed;
14
+ }
15
+
16
+ function foldBlockScalar(lines, startIndex, baseIndent) {
17
+ const values = [];
18
+ let index = startIndex + 1;
19
+
20
+ while (index < lines.length) {
21
+ const rawLine = lines[index];
22
+ if (!rawLine.trim()) {
23
+ values.push('');
24
+ index += 1;
25
+ continue;
26
+ }
27
+
28
+ const indentMatch = rawLine.match(/^(\s*)/);
29
+ const indent = indentMatch ? indentMatch[1].length : 0;
30
+ if (indent <= baseIndent) break;
31
+
32
+ values.push(rawLine.slice(baseIndent + 2).trimEnd());
33
+ index += 1;
34
+ }
35
+
36
+ const folded = values
37
+ .join('\n')
38
+ .split('\n\n')
39
+ .map((chunk) => chunk.split('\n').join(' ').trim())
40
+ .filter((chunk, idx, arr) => chunk.length > 0 || idx < arr.length - 1)
41
+ .join('\n\n')
42
+ .trim();
43
+
44
+ return { value: folded, nextIndex: index };
45
+ }
46
+
47
+ function ensureContainer(target, key) {
48
+ if (!target[key] || typeof target[key] !== 'object' || Array.isArray(target[key])) {
49
+ target[key] = {};
50
+ }
51
+ return target[key];
52
+ }
53
+
54
+ export function parseSkillFrontmatterFile(skillFilePath) {
55
+ if (!existsSync(skillFilePath)) {
56
+ throw new NotFoundError(`skill file not found: ${skillFilePath}`, { code: 'skill_not_found' });
57
+ }
58
+
59
+ const content = readFileSync(skillFilePath, 'utf-8');
60
+ if (!content.startsWith('---\n')) {
61
+ throw new ValidationError('SKILL.md missing frontmatter', { code: 'missing_frontmatter' });
62
+ }
63
+
64
+ const fmEnd = content.indexOf('\n---', 4);
65
+ if (fmEnd === -1) {
66
+ throw new ValidationError('SKILL.md has unclosed frontmatter', { code: 'unclosed_frontmatter' });
67
+ }
68
+
69
+ const lines = content.slice(4, fmEnd).split('\n');
70
+ const fields = {};
71
+ let activeArrayKey = null;
72
+ let activeArrayTarget = null;
73
+ let activeParentKey = null;
74
+
75
+ for (let index = 0; index < lines.length; index += 1) {
76
+ const rawLine = lines[index];
77
+ const line = rawLine.trimEnd();
78
+ if (!line.trim()) continue;
79
+
80
+ const listMatch = rawLine.match(/^(\s*)-\s+(.+)$/);
81
+ if (listMatch && activeArrayKey && activeArrayTarget) {
82
+ activeArrayTarget[activeArrayKey].push(parseScalar(listMatch[2]));
83
+ continue;
84
+ }
85
+
86
+ const nestedKeyMatch = rawLine.match(/^\s{2}([A-Za-z][\w-]*):\s*(.*)$/);
87
+ if (nestedKeyMatch && activeParentKey) {
88
+ const [, key, value] = nestedKeyMatch;
89
+ const parent = ensureContainer(fields, activeParentKey);
90
+ if (value === '') {
91
+ parent[key] = [];
92
+ activeArrayKey = key;
93
+ activeArrayTarget = parent;
94
+ continue;
95
+ }
96
+
97
+ parent[key] = parseScalar(value);
98
+ activeArrayKey = null;
99
+ activeArrayTarget = null;
100
+ continue;
101
+ }
102
+
103
+ const keyMatch = rawLine.match(/^([A-Za-z][\w-]*):\s*(.*)$/);
104
+ if (!keyMatch) continue;
105
+
106
+ const [, key, value] = keyMatch;
107
+ if (value === '>' || value === '|') {
108
+ const { value: blockValue, nextIndex } = foldBlockScalar(lines, index, 0);
109
+ fields[key] = blockValue;
110
+ activeParentKey = null;
111
+ activeArrayKey = null;
112
+ activeArrayTarget = null;
113
+ index = nextIndex - 1;
114
+ continue;
115
+ }
116
+
117
+ if (value === '') {
118
+ fields[key] = fields[key] && typeof fields[key] === 'object' && !Array.isArray(fields[key])
119
+ ? fields[key]
120
+ : [];
121
+ activeParentKey = key;
122
+ activeArrayKey = Array.isArray(fields[key]) ? key : null;
123
+ activeArrayTarget = Array.isArray(fields[key]) ? fields : null;
124
+ continue;
125
+ }
126
+
127
+ fields[key] = parseScalar(value);
128
+ activeParentKey = null;
129
+ activeArrayKey = null;
130
+ activeArrayTarget = null;
131
+ }
132
+
133
+ if (!fields.name) {
134
+ throw new ValidationError('SKILL.md frontmatter missing "name" field', { code: 'missing_name' });
135
+ }
136
+ if (!fields.description) {
137
+ throw new ValidationError('SKILL.md frontmatter missing "description" field', { code: 'missing_description' });
138
+ }
139
+
140
+ return {
141
+ name: fields.name,
142
+ description: fields.description,
143
+ sources: Array.isArray(fields.metadata?.sources)
144
+ ? fields.metadata.sources
145
+ : (Array.isArray(fields.sources) ? fields.sources : []),
146
+ requires: Array.isArray(fields.metadata?.requires)
147
+ ? fields.metadata.requires
148
+ : (Array.isArray(fields.requires) ? fields.requires : []),
149
+ status: typeof fields.metadata?.status === 'string' ? fields.metadata.status : null,
150
+ replacement: typeof fields.metadata?.replacement === 'string' ? fields.metadata.replacement : null,
151
+ message: typeof fields.metadata?.message === 'string' ? fields.metadata.message : null,
152
+ };
153
+ }
154
+
155
+ export function normalizeDisplayPath(repoRoot, absolutePath) {
156
+ return relative(repoRoot, absolutePath).split('\\').join('/');
157
+ }
158
+
159
+ export function normalizeRepoPath(repoRoot, absolutePath) {
160
+ return normalizeDisplayPath(repoRoot, absolutePath);
161
+ }
162
+
163
+ export function readPackageMetadata(packageDir) {
164
+ const packageJsonPath = join(packageDir, 'package.json');
165
+ if (!existsSync(packageJsonPath)) {
166
+ return {
167
+ packageName: null,
168
+ packageVersion: null,
169
+ dependencies: {},
170
+ devDependencies: {},
171
+ files: null,
172
+ repository: null,
173
+ publishConfigRegistry: null,
174
+ };
175
+ }
176
+
177
+ const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
178
+ return {
179
+ packageName: pkg.name || null,
180
+ packageVersion: pkg.version || null,
181
+ dependencies: pkg.dependencies || {},
182
+ devDependencies: pkg.devDependencies || {},
183
+ files: Array.isArray(pkg.files) ? pkg.files : null,
184
+ repository: pkg.repository || null,
185
+ publishConfigRegistry: pkg.publishConfig?.registry || null,
186
+ };
187
+ }
@@ -0,0 +1,69 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { readFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import {
5
+ readBuildState as readBuildStateRecord,
6
+ writeBuildState as writeBuildStateRecord,
7
+ } from '../../infrastructure/fs/build-state-repository.js';
8
+
9
+ export function hashFile(filePath) {
10
+ const digest = createHash('sha256').update(readFileSync(filePath)).digest('hex');
11
+ return `sha256:${digest}`;
12
+ }
13
+
14
+ export function readBuildState(repoRoot) {
15
+ return readBuildStateRecord(repoRoot);
16
+ }
17
+
18
+ export function writeBuildState(repoRoot, state) {
19
+ writeBuildStateRecord(repoRoot, state);
20
+ }
21
+
22
+ export function compareRecordedSources(repoRoot, record) {
23
+ const changes = [];
24
+ const recordedSources = record.sources || {};
25
+
26
+ for (const [sourcePath, sourceRecord] of Object.entries(recordedSources)) {
27
+ const absoluteSourcePath = join(repoRoot, sourcePath);
28
+ const currentHash = hashFile(absoluteSourcePath);
29
+ const recordedHash = sourceRecord.hash;
30
+
31
+ if (currentHash !== recordedHash) {
32
+ changes.push({
33
+ path: sourcePath,
34
+ recorded: recordedHash,
35
+ current: currentHash,
36
+ });
37
+ }
38
+ }
39
+
40
+ return changes;
41
+ }
42
+
43
+ export function buildStateRecordForPackageDir(repoRoot, packageDir, {
44
+ parseSkillFrontmatterFile,
45
+ readPackageMetadata,
46
+ normalizeDisplayPath,
47
+ } = {}) {
48
+ const skillFile = join(packageDir, 'SKILL.md');
49
+ const metadata = parseSkillFrontmatterFile(skillFile);
50
+ const packageMetadata = readPackageMetadata(packageDir);
51
+ const sources = {};
52
+
53
+ for (const sourcePath of metadata.sources) {
54
+ sources[sourcePath] = {
55
+ hash: hashFile(join(repoRoot, sourcePath)),
56
+ };
57
+ }
58
+
59
+ return {
60
+ packageName: packageMetadata.packageName,
61
+ record: {
62
+ package_version: packageMetadata.packageVersion,
63
+ skill_path: normalizeDisplayPath(repoRoot, packageDir),
64
+ skill_file: normalizeDisplayPath(repoRoot, skillFile),
65
+ sources,
66
+ requires: metadata.requires,
67
+ },
68
+ };
69
+ }
@@ -0,0 +1,16 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+
4
+ export function readBuildState(repoRoot) {
5
+ const buildStatePath = join(repoRoot, '.agentpack', 'build-state.json');
6
+ if (!existsSync(buildStatePath)) {
7
+ return { version: 1, skills: {} };
8
+ }
9
+
10
+ return JSON.parse(readFileSync(buildStatePath, 'utf-8'));
11
+ }
12
+
13
+ export function writeBuildState(repoRoot, state) {
14
+ mkdirSync(join(repoRoot, '.agentpack'), { recursive: true });
15
+ writeFileSync(join(repoRoot, '.agentpack', 'build-state.json'), JSON.stringify(state, null, 2) + '\n');
16
+ }
@@ -0,0 +1,16 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+
4
+ export function readInstallState(repoRoot) {
5
+ const installStatePath = join(repoRoot, '.agentpack', 'install.json');
6
+ if (!existsSync(installStatePath)) {
7
+ return { version: 1, installs: {} };
8
+ }
9
+
10
+ return JSON.parse(readFileSync(installStatePath, 'utf-8'));
11
+ }
12
+
13
+ export function writeInstallState(repoRoot, state) {
14
+ mkdirSync(join(repoRoot, '.agentpack'), { recursive: true });
15
+ writeFileSync(join(repoRoot, '.agentpack', 'install.json'), JSON.stringify(state, null, 2) + '\n');
16
+ }