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 +8 -1
- package/lib/installer.js +93 -17
- package/package.json +1 -1
- package/template/.agents/skills/README.md +10 -6
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
308
|
-
|
|
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
|
|
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
|
|
377
|
+
`${current}${separator}${section}\n`,
|
|
323
378
|
`append ${this.config.summaryLabel} skills to ${readmeDisplay}`
|
|
324
379
|
);
|
|
325
380
|
}
|
|
326
381
|
|
|
327
|
-
|
|
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 (!
|
|
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,9 +1,13 @@
|
|
|
1
1
|
# Project Skills
|
|
2
2
|
|
|
3
|
-
Invocation-only
|
|
3
|
+
Invocation-only - ask by name.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
- `
|
|
9
|
-
- `
|
|
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 -->
|