durable-context 1.1.0 → 1.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.
package/README.md CHANGED
@@ -28,6 +28,13 @@ Skills are invocation-only — they do not run automatically.
28
28
  Use `project-profile-baseline` once to populate repo-wide commands and
29
29
  operating facts; use `project-profile-refresh` when those stable facts change.
30
30
 
31
- Options: `--target`, `--dry-run`, `--force`, `status`.
31
+ Update managed agent assets without replacing project work:
32
+
33
+ ```bash
34
+ npx durable-context@latest update
35
+ ```
36
+
37
+ Commands/options: `update`, `status`, `--target`, `--dry-run`, and `--force`
38
+ for `init`.
32
39
 
33
40
  For release-anchored documentation, see the `reference-docs` package.
package/lib/installer.js CHANGED
@@ -36,12 +36,12 @@ export async function runCli(config, argv) {
36
36
  return;
37
37
  }
38
38
 
39
- if (args.command !== 'init') {
39
+ if (args.command !== 'init' && args.command !== 'update') {
40
40
  throw new Error(`Unknown command "${args.command}". Run "${config.cliName} --help".`);
41
41
  }
42
42
 
43
43
  const targetRoot = path.resolve(args.target);
44
- const projectName = args.projectName ?? (await inferProjectName(targetRoot));
44
+ const projectName = await resolveProjectName(config, args, targetRoot);
45
45
  const installer = new Installer({
46
46
  config,
47
47
  targetRoot,
@@ -50,7 +50,12 @@ export async function runCli(config, argv) {
50
50
  dryRun: args.dryRun
51
51
  });
52
52
 
53
- await installer.init();
53
+ if (args.command === 'init') {
54
+ await installer.init();
55
+ return;
56
+ }
57
+
58
+ await installer.update();
54
59
  }
55
60
 
56
61
  function parseArgs(argv, config) {
@@ -143,21 +148,24 @@ function printHelp(config) {
143
148
 
144
149
  Usage:
145
150
  ${config.cliName} init [options]
151
+ ${config.cliName} update [options]
146
152
  ${config.cliName} status [options]
147
153
 
148
154
  Options:
149
155
  --target <path> Project root to install into. Defaults to cwd.
150
156
  --project-name <name> Name used to replace PROJECT_NAME placeholders.
151
- --force Replace existing generated directories.
157
+ --force Replace existing generated directories during init.
152
158
  --dry-run Show planned changes without writing files.
153
159
  -h, --help Show help.
154
160
  -v, --version Show package version.
155
161
 
156
162
  Examples:
157
163
  npx ${config.packageJson.name} init --project-name "My App"
164
+ npx ${config.packageJson.name}@latest update
158
165
  npx ${config.packageJson.name}@${config.packageJson.version} init --project-name "My App"
159
166
  npx ${config.packageJson.name} status --target ../existing-project
160
167
 
168
+ The update command refreshes managed agent assets without replacing project work.
161
169
  The status command reads ${config.metadataPath} from an initialized project.
162
170
  `);
163
171
  }
@@ -176,6 +184,22 @@ async function inferProjectName(targetRoot) {
176
184
  return path.basename(targetRoot);
177
185
  }
178
186
 
187
+ async function resolveProjectName(config, args, targetRoot) {
188
+ if (args.projectName) {
189
+ return args.projectName;
190
+ }
191
+
192
+ if (args.command === 'update') {
193
+ const metadata = await readOptionalJson(path.join(targetRoot, config.metadataPath));
194
+
195
+ if (typeof metadata?.projectName === 'string' && metadata.projectName.trim()) {
196
+ return metadata.projectName;
197
+ }
198
+ }
199
+
200
+ return inferProjectName(targetRoot);
201
+ }
202
+
179
203
  class Installer {
180
204
  constructor({ config, targetRoot, projectName, force, dryRun }) {
181
205
  this.config = config;
@@ -199,6 +223,16 @@ class Installer {
199
223
  this.printSummary();
200
224
  }
201
225
 
226
+ async update() {
227
+ await this.ensureDirectory(this.targetRoot);
228
+
229
+ await this.installAgentsFile();
230
+ await this.updateSkills();
231
+ await this.writeMetadata();
232
+
233
+ this.printSummary();
234
+ }
235
+
202
236
  async installPayloadRoots() {
203
237
  const entries = await readdir(this.templateDir, { withFileTypes: true });
204
238
 
@@ -290,6 +324,22 @@ class Installer {
290
324
  );
291
325
  }
292
326
 
327
+ await this.upsertSkillsReadme();
328
+ }
329
+
330
+ async updateSkills() {
331
+ for (const skill of this.config.skills) {
332
+ await this.copyTemplatePath(
333
+ `${agentsSkillsRelative}/${skill.name}`,
334
+ `${agentsSkillsRelative}/${skill.name}`,
335
+ { replaceExisting: true }
336
+ );
337
+ }
338
+
339
+ await this.upsertSkillsReadme();
340
+ }
341
+
342
+ async upsertSkillsReadme() {
293
343
  const readmeTarget = await this.findExistingTargetPath(`${agentsSkillsRelative}/README.md`);
294
344
  const readmePath = readmeTarget.exists
295
345
  ? readmeTarget.path
@@ -302,29 +352,55 @@ class Installer {
302
352
  }
303
353
 
304
354
  const current = await readFile(readmePath, 'utf8');
305
- const missingSkills = this.config.skills.filter((skill) => !current.includes(skill.name));
355
+ const section = this.renderSkillsReadmeSection();
356
+ const { start, end } = this.skillsReadmeMarkers();
357
+
358
+ if (current.includes(start) && current.includes(end)) {
359
+ const updated = current.replace(
360
+ new RegExp(`${escapeRegExp(start)}[\\s\\S]*?${escapeRegExp(end)}`),
361
+ section
362
+ );
306
363
 
307
- if (missingSkills.length === 0) {
308
- this.note(`${readmeDisplay} already lists the ${this.config.summaryLabel} skills`);
364
+ if (updated === current) {
365
+ this.note(`${readmeDisplay} already has the ${this.config.summaryLabel} skills section`);
366
+ return;
367
+ }
368
+
369
+ await this.writeFile(readmePath, updated, `update ${readmeDisplay} ${this.config.summaryLabel} skills section`);
309
370
  return;
310
371
  }
311
372
 
312
- const entry = [
313
- '',
314
- `## ${this.config.summaryLabel} Skills`,
315
- '',
316
- ...missingSkills.map((skill) => skill.readmeEntry),
317
- ''
318
- ].join('\n');
373
+ const separator = current.endsWith('\n\n') ? '' : current.endsWith('\n') ? '\n' : '\n\n';
319
374
 
320
375
  await this.writeFile(
321
376
  readmePath,
322
- `${current.trimEnd()}\n${entry}`,
377
+ `${current}${separator}${section}\n`,
323
378
  `append ${this.config.summaryLabel} skills to ${readmeDisplay}`
324
379
  );
325
380
  }
326
381
 
327
- async copyTemplatePath(sourceRelative, targetRelative) {
382
+ skillsReadmeMarkers() {
383
+ const name = this.config.packageJson.name;
384
+
385
+ return {
386
+ start: `<!-- ${name}:skills:start -->`,
387
+ end: `<!-- ${name}:skills:end -->`
388
+ };
389
+ }
390
+
391
+ renderSkillsReadmeSection() {
392
+ const { start, end } = this.skillsReadmeMarkers();
393
+
394
+ return [
395
+ start,
396
+ `## ${this.config.summaryLabel} Skills`,
397
+ '',
398
+ ...this.config.skills.map((skill) => skill.readmeEntry),
399
+ end
400
+ ].join('\n');
401
+ }
402
+
403
+ async copyTemplatePath(sourceRelative, targetRelative, { replaceExisting = this.force } = {}) {
328
404
  const sourcePath = path.join(this.templateDir, sourceRelative);
329
405
  const targetInfo = await this.findExistingTargetPath(targetRelative);
330
406
 
@@ -337,7 +413,7 @@ class Installer {
337
413
  if (targetInfo.exists) {
338
414
  const variantNote = targetInfo.caseVariant ? ` at ${targetInfo.display}` : '';
339
415
 
340
- if (!this.force) {
416
+ if (!replaceExisting) {
341
417
  this.note(`skip ${targetRelative} (already exists${variantNote}; use --force to replace)`);
342
418
  return false;
343
419
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "durable-context",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "description": "Invocation-only skills and scaffold for durable planning: initiatives in context/, decisions in an append-only log.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -1,9 +1,13 @@
1
1
  # Project Skills
2
2
 
3
- Invocation-only ask by name.
3
+ Invocation-only - ask by name.
4
4
 
5
- - `project-profile-baseline` — populate `context/project-profile.md` from source-backed repo facts.
6
- - `project-profile-refresh` refresh stable repo-wide profile facts after repo changes or release documentation refreshes.
7
- - `plan-with-context` — draft a durable plan in an initiative `plan.md`.
8
- - `devils-advocate` critique a draft plan before distribution.
9
- - `dive-into-plan` interrogate a settled plan, distribute into initiative docs, promote to `decisions/`.
5
+ <!-- durable-context:skills:start -->
6
+ ## Durable Context Skills
7
+
8
+ - `project-profile-baseline` - invoke explicitly to populate `context/project-profile.md` from source-backed repo facts.
9
+ - `project-profile-refresh` - invoke explicitly to refresh stable repo-wide facts in `context/project-profile.md`.
10
+ - `plan-with-context` - invoke explicitly to draft a durable plan in an initiative `plan.md`.
11
+ - `devils-advocate` - invoke explicitly to challenge a draft plan before distribution.
12
+ - `dive-into-plan` - invoke explicitly to interrogate a settled plan, distribute it into initiative docs, and promote decisions.
13
+ <!-- durable-context:skills:end -->