@stonepandastudio/cairn 0.3.0 → 0.4.0
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 +9 -8
- package/lib/config.js +18 -1
- package/lib/doctor/index.js +8 -5
- package/lib/init.js +22 -10
- package/package.json +1 -1
- package/schema.json +6 -3
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ agreeing.
|
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
14
|
npm i -D @stonepandastudio/cairn
|
|
15
|
-
npx cairn init --stack
|
|
15
|
+
npx cairn init --stack nestjs,typeorm --tracker jira-server --project-key MYPROJ --story-type Task
|
|
16
16
|
```
|
|
17
17
|
|
|
18
18
|
## What v1 does
|
|
@@ -134,16 +134,17 @@ overwriting a hand edit is the destructive outcome.
|
|
|
134
134
|
|
|
135
135
|
### Workspace config (`repos.json`)
|
|
136
136
|
|
|
137
|
-
- `repos[]` — name, path, stack
|
|
138
|
-
|
|
139
|
-
|
|
137
|
+
- `repos[]` — name, path, stack (`"angular"` or a list `["nestjs", "typeorm"]` —
|
|
138
|
+
framework first). Per-repo settings live in each repo's own `cairn.config.json`;
|
|
139
|
+
the inline `vars` blocks are a fallback used only until a repo has been through
|
|
140
|
+
`cairn init`, so migration can happen one repo at a time.
|
|
140
141
|
- `pathAliases` — local → canonical. When one repo names a stub `execute-tests.md`
|
|
141
142
|
where the others use `execute-test-plan.md`, aliasing folds them onto the cohort's
|
|
142
143
|
names instead of reporting one `MISSING` and one `STRANDED` for the same file.
|
|
143
144
|
- `shared[]` — glob rules. `cohort: "all"` compares every repo; `cohort: "stack"`
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
never skipped on that basis.
|
|
145
|
+
groups by the first stack entry (all `nestjs` repos together, whatever the ORM).
|
|
146
|
+
`requires: { tracker: true }` skips repos with no tracker. A repo with no config
|
|
147
|
+
has an *unknown* tracker, not an absent one, and is never skipped on that basis.
|
|
147
148
|
- Anything not matched by a rule — `ai/contexts/`, `ai/progress/`, `ai/tasks/`,
|
|
148
149
|
`DATABASE_SCHEMA.md` — is per-project by design and never inspected.
|
|
149
150
|
`ai/INITIAL_PROMPT.md` is explicitly excluded: variants at ~0% shared content make
|
|
@@ -167,4 +168,4 @@ schema.json JSON Schema for cairn.config.json
|
|
|
167
168
|
test/run.js dependency-free test runner
|
|
168
169
|
```
|
|
169
170
|
|
|
170
|
-
No runtime dependencies. Node >= 18. `npm test` runs
|
|
171
|
+
No runtime dependencies. Node >= 18. `npm test` runs 51 tests.
|
package/lib/config.js
CHANGED
|
@@ -13,7 +13,7 @@ const CAIRN_DIR = '_cairn';
|
|
|
13
13
|
// generated directory from slowly acquiring hand-maintained state.
|
|
14
14
|
|
|
15
15
|
const DEFAULTS = {
|
|
16
|
-
stack: 'unknown',
|
|
16
|
+
stack: ['unknown'],
|
|
17
17
|
tracker: { provider: 'none' },
|
|
18
18
|
agents: {},
|
|
19
19
|
vars: {},
|
|
@@ -21,6 +21,21 @@ const DEFAULTS = {
|
|
|
21
21
|
|
|
22
22
|
class ConfigError extends Error {}
|
|
23
23
|
|
|
24
|
+
// `stack` is a list — the framework first, then composable add-ons
|
|
25
|
+
// (`["nestjs", "typeorm"]`). A bare string or a comma-separated string is
|
|
26
|
+
// accepted and normalized. The doctor's `cohort: "stack"` groups by the first
|
|
27
|
+
// entry; the v2 renderer layers a preset per entry, in order.
|
|
28
|
+
function normalizeStack(value) {
|
|
29
|
+
if (Array.isArray(value)) {
|
|
30
|
+
const list = value.map((s) => String(s).trim()).filter(Boolean);
|
|
31
|
+
return list.length ? list : ['unknown'];
|
|
32
|
+
}
|
|
33
|
+
if (typeof value === 'string' && value.trim()) {
|
|
34
|
+
return value.split(',').map((s) => s.trim()).filter(Boolean);
|
|
35
|
+
}
|
|
36
|
+
return ['unknown'];
|
|
37
|
+
}
|
|
38
|
+
|
|
24
39
|
function configPath(repoPath) {
|
|
25
40
|
return path.join(repoPath, CONFIG_NAME);
|
|
26
41
|
}
|
|
@@ -110,6 +125,7 @@ function loadRepoConfig(repoPath, { fallbackVars = null, required = false } = {}
|
|
|
110
125
|
const cfg = {
|
|
111
126
|
...DEFAULTS,
|
|
112
127
|
...raw,
|
|
128
|
+
stack: normalizeStack(raw.stack),
|
|
113
129
|
tracker: normalizeTracker(raw.tracker),
|
|
114
130
|
agents: raw.agents || {},
|
|
115
131
|
vars: raw.vars || fallbackVars || {},
|
|
@@ -142,5 +158,6 @@ module.exports = {
|
|
|
142
158
|
findRepoRoot,
|
|
143
159
|
loadEnv,
|
|
144
160
|
loadRepoConfig,
|
|
161
|
+
normalizeStack,
|
|
145
162
|
parseEnv,
|
|
146
163
|
};
|
package/lib/doctor/index.js
CHANGED
|
@@ -20,7 +20,7 @@ const { matchRule, readFile } = require('./scan');
|
|
|
20
20
|
const { buildNormalizer } = require('./normalize');
|
|
21
21
|
const diff = require('./diff');
|
|
22
22
|
const { makePaint, stripAnsi, table } = require('../paint');
|
|
23
|
-
const { loadRepoConfig } = require('../config');
|
|
23
|
+
const { loadRepoConfig, normalizeStack } = require('../config');
|
|
24
24
|
const { readManifest, statusFor } = require('../manifest');
|
|
25
25
|
|
|
26
26
|
function hash(s) {
|
|
@@ -55,7 +55,8 @@ function resolveRepos(config, configDir) {
|
|
|
55
55
|
return {
|
|
56
56
|
name: entry.name,
|
|
57
57
|
path: repoPath.replace(/\\/g, '/'),
|
|
58
|
-
|
|
58
|
+
// A list — framework first, then add-ons. `cohort: "stack"` groups by [0].
|
|
59
|
+
stack: normalizeStack(entry.stack || repoConfig.stack),
|
|
59
60
|
vars: { ...(repoConfig.vars || {}), ...(entry.vars || {}) },
|
|
60
61
|
tracker: repoConfig.tracker,
|
|
61
62
|
managed: !repoConfig.unmanaged,
|
|
@@ -72,7 +73,9 @@ function cohortGroups(rule, repos) {
|
|
|
72
73
|
if (rule.cohort === 'stack') {
|
|
73
74
|
const byStack = new Map();
|
|
74
75
|
for (const repo of repos) {
|
|
75
|
-
|
|
76
|
+
// Compare at framework granularity — the shared contracts live there, and
|
|
77
|
+
// ORM/add-on differences are resolved by composing presets, not by cohort.
|
|
78
|
+
const key = (repo.stack && repo.stack[0]) || 'unknown';
|
|
76
79
|
if (!byStack.has(key)) byStack.set(key, []);
|
|
77
80
|
byStack.get(key).push(repo);
|
|
78
81
|
}
|
|
@@ -94,7 +97,7 @@ function repoMeetsRequirements(repo, rule) {
|
|
|
94
97
|
if (req.tracker === true && repo.managed && (!repo.tracker || repo.tracker.provider === 'none')) {
|
|
95
98
|
return false;
|
|
96
99
|
}
|
|
97
|
-
if (req.stack && repo.stack
|
|
100
|
+
if (req.stack && !(repo.stack || []).includes(req.stack)) return false;
|
|
98
101
|
return true;
|
|
99
102
|
}
|
|
100
103
|
|
|
@@ -514,4 +517,4 @@ function main(argv = process.argv.slice(2)) {
|
|
|
514
517
|
return 0;
|
|
515
518
|
}
|
|
516
519
|
|
|
517
|
-
module.exports = { main, analyze, analyzeManaged, resolveRepos, render, HELP, stripAnsi };
|
|
520
|
+
module.exports = { main, analyze, analyzeManaged, resolveRepos, cohortGroups, render, HELP, stripAnsi };
|
package/lib/init.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
|
|
6
|
-
const { CONFIG_NAME, configPath, cairnDir } = require('./config');
|
|
6
|
+
const { CONFIG_NAME, configPath, cairnDir, normalizeStack } = require('./config');
|
|
7
7
|
const {
|
|
8
8
|
emptyManifest,
|
|
9
9
|
readManifest,
|
|
@@ -29,19 +29,29 @@ const { makePaint } = require('./paint');
|
|
|
29
29
|
|
|
30
30
|
const SHIM_SOURCE = 'shims/jira.js';
|
|
31
31
|
const SHIM_TARGET = '_cairn/scripts/jira.js';
|
|
32
|
-
|
|
32
|
+
|
|
33
|
+
// Copied tracker-client filenames seen across the repos. `cairn init` replaces
|
|
34
|
+
// every one that exists with the shim — the snap-proof repos carry all three
|
|
35
|
+
// (`task-tracker.js` dispatching to `jira.js` / `youtrack.js`), and the docs
|
|
36
|
+
// that reference them by path keep working because the shim reads the same
|
|
37
|
+
// argv. A repo that never had one is left alone; the shim is never invented.
|
|
38
|
+
const LEGACY_SHIM_TARGETS = [
|
|
39
|
+
'ai/scripts/jira.js',
|
|
40
|
+
'ai/scripts/youtrack.js',
|
|
41
|
+
'ai/scripts/task-tracker.js',
|
|
42
|
+
];
|
|
33
43
|
|
|
34
44
|
const HELP = `cairn init — make a repo cairn-managed
|
|
35
45
|
|
|
36
46
|
Usage: cairn init [options]
|
|
37
47
|
|
|
38
48
|
--repo <path> repo to initialise (default: cwd)
|
|
39
|
-
--stack <
|
|
49
|
+
--stack <names> comma-separated, framework first: nestjs,typeorm | angular | cli
|
|
40
50
|
--tracker <provider> ${listProviders().join(' | ')} (default: none)
|
|
41
51
|
--project-key <KEY> tracker project key, e.g. PROOF
|
|
42
52
|
--story-type <name> parent issue type (default: Story)
|
|
43
53
|
--subtask-type <name> child issue type (default: Sub-task)
|
|
44
|
-
--no-shim do not vendor the ai/scripts
|
|
54
|
+
--no-shim do not vendor or replace the ai/scripts/*.js client shims
|
|
45
55
|
--force overwrite an existing cairn.config.json
|
|
46
56
|
--dry-run print what would be written, write nothing
|
|
47
57
|
`;
|
|
@@ -84,7 +94,7 @@ function buildConfig(args) {
|
|
|
84
94
|
const cfg = {
|
|
85
95
|
$schema: `./node_modules/${require('../package.json').name}/schema.json`,
|
|
86
96
|
cairn: require('../package.json').version,
|
|
87
|
-
stack: args.stack,
|
|
97
|
+
stack: normalizeStack(args.stack),
|
|
88
98
|
tracker: { provider: args.tracker },
|
|
89
99
|
};
|
|
90
100
|
if (args.tracker !== 'none') {
|
|
@@ -140,10 +150,12 @@ function main(argv = process.argv.slice(2)) {
|
|
|
140
150
|
const shim = readTemplate(SHIM_SOURCE);
|
|
141
151
|
if (args.shim) {
|
|
142
152
|
planned.push([SHIM_TARGET, shim, SHIM_SOURCE]);
|
|
143
|
-
// Only replace
|
|
144
|
-
//
|
|
145
|
-
|
|
146
|
-
|
|
153
|
+
// Only replace a legacy path if a copied client is actually there — creating
|
|
154
|
+
// one in a repo that never had it would invent a path nothing references.
|
|
155
|
+
for (const target of LEGACY_SHIM_TARGETS) {
|
|
156
|
+
if (fs.existsSync(path.join(repoPath, target))) {
|
|
157
|
+
planned.push([target, shim, SHIM_SOURCE]);
|
|
158
|
+
}
|
|
147
159
|
}
|
|
148
160
|
}
|
|
149
161
|
|
|
@@ -186,4 +198,4 @@ function main(argv = process.argv.slice(2)) {
|
|
|
186
198
|
return 0;
|
|
187
199
|
}
|
|
188
200
|
|
|
189
|
-
module.exports = { main, buildConfig, HELP, SHIM_TARGET,
|
|
201
|
+
module.exports = { main, buildConfig, HELP, SHIM_TARGET, LEGACY_SHIM_TARGETS, cairnDir };
|
package/package.json
CHANGED
package/schema.json
CHANGED
|
@@ -12,9 +12,12 @@
|
|
|
12
12
|
"description": "Version of @stonepandastudio/cairn this repo is pinned to."
|
|
13
13
|
},
|
|
14
14
|
"stack": {
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
|
|
15
|
+
"description": "Framework first, then composable add-ons. `cohort: \"stack\"` in the doctor groups by the first entry; the v2 renderer layers one preset per entry, in order. A bare or comma-separated string is also accepted.",
|
|
16
|
+
"anyOf": [
|
|
17
|
+
{ "type": "string" },
|
|
18
|
+
{ "type": "array", "items": { "type": "string" }, "minItems": 1 }
|
|
19
|
+
],
|
|
20
|
+
"examples": ["angular", ["nestjs", "typeorm"], "cli"]
|
|
18
21
|
},
|
|
19
22
|
"tracker": {
|
|
20
23
|
"type": "object",
|