@stonepandastudio/cairn 0.4.0 → 0.4.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 +4 -2
- package/lib/config.js +4 -0
- package/lib/init.js +57 -19
- package/lib/tracker/index.js +17 -2
- package/lib/tracker/jira-server.js +6 -5
- package/lib/tracker/youtrack.js +9 -5
- package/package.json +1 -1
- package/schema.json +6 -0
package/README.md
CHANGED
|
@@ -79,7 +79,9 @@ Credentials come from the repo's `.env` — `JIRA_BASE_URL` / `JIRA_USER` /
|
|
|
79
79
|
`JIRA_PASSWORD` for `jira-server` (Basic auth: Jira Server 8.5.1 predates PATs), or
|
|
80
80
|
`YOUTRACK_URL` / `YOUTRACK_TOKEN` / `YOUTRACK_PROJECT` for `youtrack` (Bearer token,
|
|
81
81
|
admin-read scope for the project and stage-bundle lookups). Anything already in the
|
|
82
|
-
environment wins over the file.
|
|
82
|
+
environment wins over the file. If the repo's own app reads those same names, set
|
|
83
|
+
`tracker.envPrefix` (`cairn init --env-prefix CAIRN_`) so cairn reads
|
|
84
|
+
`CAIRN_YOUTRACK_TOKEN` instead — or point `tracker.env` at a separate file.
|
|
83
85
|
|
|
84
86
|
### Legacy command names
|
|
85
87
|
|
|
@@ -168,4 +170,4 @@ schema.json JSON Schema for cairn.config.json
|
|
|
168
170
|
test/run.js dependency-free test runner
|
|
169
171
|
```
|
|
170
172
|
|
|
171
|
-
No runtime dependencies. Node >= 18. `npm test` runs
|
|
173
|
+
No runtime dependencies. Node >= 18. `npm test` runs 56 tests.
|
package/lib/config.js
CHANGED
|
@@ -84,6 +84,10 @@ function normalizeTracker(tracker) {
|
|
|
84
84
|
t.issueTypes = { story: 'Story', subtask: 'Sub-task', ...(t.issueTypes || {}) };
|
|
85
85
|
t.statuses = { todo: 'To Do', inProgress: 'In Progress', done: 'Done', ...(t.statuses || {}) };
|
|
86
86
|
t.env = t.env || '.env';
|
|
87
|
+
// Prepended to every credential var name the provider reads — set it when the
|
|
88
|
+
// repo's app talks to the same tracker with its own credentials and `JIRA_*` /
|
|
89
|
+
// `YOUTRACK_*` would collide. Empty by default.
|
|
90
|
+
t.envPrefix = t.envPrefix || '';
|
|
87
91
|
return t;
|
|
88
92
|
}
|
|
89
93
|
|
package/lib/init.js
CHANGED
|
@@ -10,22 +10,25 @@ const {
|
|
|
10
10
|
record,
|
|
11
11
|
writeManifest,
|
|
12
12
|
manifestPath,
|
|
13
|
+
hashContent,
|
|
13
14
|
} = require('./manifest');
|
|
14
15
|
const { listProviders } = require('./tracker');
|
|
15
16
|
const { makePaint } = require('./paint');
|
|
16
17
|
|
|
17
18
|
// `cairn init` — make a repo cairn-managed.
|
|
18
19
|
//
|
|
19
|
-
// It writes
|
|
20
|
+
// It writes:
|
|
20
21
|
// cairn.config.json hand-editable from here on; cairn never rewrites it
|
|
22
|
+
// except with --force
|
|
21
23
|
// _cairn/scripts/jira.js vendored shim, tracked in the manifest
|
|
22
24
|
// _cairn/manifest.json the record of what was written
|
|
23
25
|
//
|
|
24
|
-
// Plus,
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
26
|
+
// Plus, for every copied client the repo still carries (jira.js / youtrack.js /
|
|
27
|
+
// task-tracker.js under ai/scripts/), that file is replaced by the same shim —
|
|
28
|
+
// replacing rather than deleting because dozens of prose references name the path
|
|
29
|
+
// and the shim reads the same argv. A file already holding the shim is skipped; a
|
|
30
|
+
// file hand-edited away from what cairn wrote is kept and reported unless
|
|
31
|
+
// --force-shims. cairn never invents one of these paths in a repo that lacked it.
|
|
29
32
|
|
|
30
33
|
const SHIM_SOURCE = 'shims/jira.js';
|
|
31
34
|
const SHIM_TARGET = '_cairn/scripts/jira.js';
|
|
@@ -49,10 +52,14 @@ Usage: cairn init [options]
|
|
|
49
52
|
--stack <names> comma-separated, framework first: nestjs,typeorm | angular | cli
|
|
50
53
|
--tracker <provider> ${listProviders().join(' | ')} (default: none)
|
|
51
54
|
--project-key <KEY> tracker project key, e.g. PROOF
|
|
52
|
-
--story-type <name> parent issue type (default: Story)
|
|
53
|
-
--subtask-type <name> child issue type (default: Sub-task)
|
|
55
|
+
--story-type <name> parent issue type (default: Story, jira-server only)
|
|
56
|
+
--subtask-type <name> child issue type (default: Sub-task, jira-server only)
|
|
57
|
+
--env-prefix <PFX> prefix for the tracker credential vars, e.g. CAIRN_
|
|
58
|
+
(only when the repo's app reads JIRA_*/YOUTRACK_* itself)
|
|
54
59
|
--no-shim do not vendor or replace the ai/scripts/*.js client shims
|
|
55
60
|
--force overwrite an existing cairn.config.json
|
|
61
|
+
--force-shims also replace an ai/scripts/*.js shim that was hand-edited
|
|
62
|
+
since cairn wrote it (kept untouched otherwise)
|
|
56
63
|
--dry-run print what would be written, write nothing
|
|
57
64
|
`;
|
|
58
65
|
|
|
@@ -64,8 +71,10 @@ function parseArgs(argv) {
|
|
|
64
71
|
projectKey: null,
|
|
65
72
|
storyType: 'Story',
|
|
66
73
|
subtaskType: 'Sub-task',
|
|
74
|
+
envPrefix: '',
|
|
67
75
|
shim: true,
|
|
68
76
|
force: false,
|
|
77
|
+
forceShims: false,
|
|
69
78
|
dryRun: false,
|
|
70
79
|
color: process.stdout.isTTY,
|
|
71
80
|
};
|
|
@@ -77,8 +86,10 @@ function parseArgs(argv) {
|
|
|
77
86
|
else if (a === '--project-key') args.projectKey = argv[++i];
|
|
78
87
|
else if (a === '--story-type') args.storyType = argv[++i];
|
|
79
88
|
else if (a === '--subtask-type') args.subtaskType = argv[++i];
|
|
89
|
+
else if (a === '--env-prefix') args.envPrefix = argv[++i];
|
|
80
90
|
else if (a === '--no-shim') args.shim = false;
|
|
81
91
|
else if (a === '--force') args.force = true;
|
|
92
|
+
else if (a === '--force-shims') args.forceShims = args.force = true;
|
|
82
93
|
else if (a === '--dry-run') args.dryRun = true;
|
|
83
94
|
else if (a === '--no-color') args.color = false;
|
|
84
95
|
else if (a === '-h' || a === '--help') args.help = true;
|
|
@@ -100,6 +111,7 @@ function buildConfig(args) {
|
|
|
100
111
|
if (args.tracker !== 'none') {
|
|
101
112
|
cfg.tracker.projectKey = args.projectKey;
|
|
102
113
|
cfg.tracker.env = '.env';
|
|
114
|
+
if (args.envPrefix) cfg.tracker.envPrefix = args.envPrefix;
|
|
103
115
|
// issueTypes is a Jira concept — YouTrack has no Task/Sub-task split.
|
|
104
116
|
if (args.tracker === 'jira-server') {
|
|
105
117
|
cfg.tracker.issueTypes = { story: args.storyType, subtask: args.subtaskType };
|
|
@@ -143,29 +155,52 @@ function main(argv = process.argv.slice(2)) {
|
|
|
143
155
|
return 2;
|
|
144
156
|
}
|
|
145
157
|
|
|
158
|
+
const manifest = readManifest(repoPath) || emptyManifest();
|
|
159
|
+
|
|
146
160
|
const planned = [];
|
|
161
|
+
const kept = [];
|
|
147
162
|
const config = buildConfig(args);
|
|
148
163
|
planned.push([CONFIG_NAME, JSON.stringify(config, null, 2) + '\n', null]);
|
|
149
164
|
|
|
150
165
|
const shim = readTemplate(SHIM_SOURCE);
|
|
151
166
|
if (args.shim) {
|
|
152
167
|
planned.push([SHIM_TARGET, shim, SHIM_SOURCE]);
|
|
153
|
-
//
|
|
154
|
-
//
|
|
168
|
+
// Replace a legacy client only where one is actually present. A file already
|
|
169
|
+
// holding the shim is left alone; one hand-edited away from what cairn wrote
|
|
170
|
+
// is kept unless --force — never silently overwrite an edit.
|
|
155
171
|
for (const target of LEGACY_SHIM_TARGETS) {
|
|
156
|
-
|
|
157
|
-
|
|
172
|
+
const abs = path.join(repoPath, target);
|
|
173
|
+
if (!fs.existsSync(abs)) continue;
|
|
174
|
+
const current = fs.readFileSync(abs, 'utf8');
|
|
175
|
+
const entry = manifest.files[target];
|
|
176
|
+
|
|
177
|
+
// Already the shim and already tracked — nothing to do. If it holds the
|
|
178
|
+
// shim but isn't in the manifest (someone copied it by hand), fall through
|
|
179
|
+
// so this run records it.
|
|
180
|
+
if (current === shim && entry) continue;
|
|
181
|
+
|
|
182
|
+
const modified = entry && hashContent(current) !== entry.hash;
|
|
183
|
+
if (modified && !args.forceShims) {
|
|
184
|
+
kept.push(target);
|
|
185
|
+
continue;
|
|
158
186
|
}
|
|
187
|
+
const note =
|
|
188
|
+
current === shim ? 'recording existing shim' : entry ? null : `replacing ${current.split('\n').length} lines`;
|
|
189
|
+
planned.push([target, shim, SHIM_SOURCE, note]);
|
|
159
190
|
}
|
|
160
191
|
}
|
|
161
192
|
|
|
162
193
|
if (args.dryRun) {
|
|
163
194
|
console.log(paint.bold(`cairn init --dry-run ${repoPath}`));
|
|
164
|
-
for (const [rel] of planned)
|
|
195
|
+
for (const [rel, , , note] of planned) {
|
|
196
|
+
console.log(` would write ${rel}${note ? paint.dim(` (${note})`) : ''}`);
|
|
197
|
+
}
|
|
198
|
+
for (const rel of kept) {
|
|
199
|
+
console.log(paint.yellow(` would keep ${rel} (hand-edited — pass --force-shims to replace)`));
|
|
200
|
+
}
|
|
165
201
|
return 0;
|
|
166
202
|
}
|
|
167
203
|
|
|
168
|
-
const manifest = readManifest(repoPath) || emptyManifest();
|
|
169
204
|
for (const [rel, content, source] of planned) {
|
|
170
205
|
const abs = path.join(repoPath, rel);
|
|
171
206
|
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
@@ -177,10 +212,14 @@ function main(argv = process.argv.slice(2)) {
|
|
|
177
212
|
writeManifest(repoPath, manifest);
|
|
178
213
|
|
|
179
214
|
console.log(paint.bold(`cairn init ${repoPath}`));
|
|
180
|
-
for (const [rel, , source] of planned) {
|
|
181
|
-
|
|
215
|
+
for (const [rel, , source, note] of planned) {
|
|
216
|
+
const tag = note || source;
|
|
217
|
+
console.log(` ${paint.green('wrote')} ${rel}${tag ? paint.dim(` (${tag})`) : ''}`);
|
|
182
218
|
}
|
|
183
219
|
console.log(` ${paint.green('wrote')} ${path.relative(repoPath, manifestPath(repoPath)).replace(/\\/g, '/')}`);
|
|
220
|
+
for (const rel of kept) {
|
|
221
|
+
console.log(paint.yellow(` kept ${rel} (hand-edited since cairn wrote it — pass --force-shims to replace)`));
|
|
222
|
+
}
|
|
184
223
|
|
|
185
224
|
const gitignore = path.join(repoPath, '.gitignore');
|
|
186
225
|
const hasIgnore = fs.existsSync(gitignore) && /^_cairn\b/m.test(fs.readFileSync(gitignore, 'utf8'));
|
|
@@ -190,9 +229,8 @@ function main(argv = process.argv.slice(2)) {
|
|
|
190
229
|
);
|
|
191
230
|
}
|
|
192
231
|
if (args.tracker !== 'none') {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
);
|
|
232
|
+
const envVars = require('./tracker').providerEnv(args.tracker, args.envPrefix);
|
|
233
|
+
console.log(paint.dim(`\n Next: ensure .env has ${envVars.join(', ')}, then run`));
|
|
196
234
|
console.log(paint.dim(` npx cairn tracker list-statuses ${args.projectKey}`));
|
|
197
235
|
}
|
|
198
236
|
return 0;
|
package/lib/tracker/index.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { loadEnv, loadRepoConfig, findRepoRoot, ConfigError } = require('../config');
|
|
4
|
-
const
|
|
5
|
-
const
|
|
4
|
+
const jiraServer = require('./jira-server');
|
|
5
|
+
const youtrack = require('./youtrack');
|
|
6
|
+
const { createJiraServer, TrackerError, NoTransitionError } = jiraServer;
|
|
7
|
+
const { createYouTrack } = youtrack;
|
|
6
8
|
const { createNone } = require('./none');
|
|
7
9
|
|
|
8
10
|
// Provider registry. Adding Linear or GitHub Issues later means adding one entry
|
|
@@ -18,6 +20,18 @@ function listProviders() {
|
|
|
18
20
|
return Object.keys(PROVIDERS);
|
|
19
21
|
}
|
|
20
22
|
|
|
23
|
+
// The .env variables a provider reads. `cairn init` prints these as the next step,
|
|
24
|
+
// so a youtrack repo is not told to set JIRA_* and vice versa.
|
|
25
|
+
const CREDENTIAL_ENV = {
|
|
26
|
+
'jira-server': jiraServer.CREDENTIAL_ENV,
|
|
27
|
+
youtrack: youtrack.CREDENTIAL_ENV,
|
|
28
|
+
none: [],
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
function providerEnv(name, prefix = '') {
|
|
32
|
+
return (CREDENTIAL_ENV[name] || []).map((v) => prefix + v);
|
|
33
|
+
}
|
|
34
|
+
|
|
21
35
|
// Build a tracker straight from a tracker config block plus an env bag.
|
|
22
36
|
function createTracker(trackerConfig = {}, env = process.env) {
|
|
23
37
|
const name = trackerConfig.provider || 'none';
|
|
@@ -48,6 +62,7 @@ module.exports = {
|
|
|
48
62
|
createTracker,
|
|
49
63
|
trackerForRepo,
|
|
50
64
|
listProviders,
|
|
65
|
+
providerEnv,
|
|
51
66
|
TrackerError,
|
|
52
67
|
NoTransitionError,
|
|
53
68
|
};
|
|
@@ -38,14 +38,15 @@ class NoTransitionError extends TrackerError {
|
|
|
38
38
|
const REQUIRED_ENV = ['JIRA_BASE_URL', 'JIRA_USER', 'JIRA_PASSWORD'];
|
|
39
39
|
|
|
40
40
|
function createJiraServer(config = {}, env = process.env) {
|
|
41
|
-
const
|
|
42
|
-
const
|
|
43
|
-
const
|
|
41
|
+
const pfx = config.envPrefix || '';
|
|
42
|
+
const baseUrl = (config.baseUrl || env[pfx + 'JIRA_BASE_URL'] || '').replace(/\/+$/, '');
|
|
43
|
+
const user = config.user || env[pfx + 'JIRA_USER'];
|
|
44
|
+
const password = config.password || env[pfx + 'JIRA_PASSWORD'];
|
|
44
45
|
const projectKey = config.projectKey;
|
|
45
46
|
const issueTypes = { story: 'Story', subtask: 'Sub-task', ...(config.issueTypes || {}) };
|
|
46
47
|
|
|
47
48
|
function assertConfig() {
|
|
48
|
-
const missing = REQUIRED_ENV.filter((k, i) => ![baseUrl, user, password][i]);
|
|
49
|
+
const missing = REQUIRED_ENV.filter((k, i) => ![baseUrl, user, password][i]).map((k) => pfx + k);
|
|
49
50
|
if (missing.length) {
|
|
50
51
|
throw new TrackerError(
|
|
51
52
|
`Missing Jira credentials: ${missing.join(', ')}. Set them in the repo's .env or the environment.`,
|
|
@@ -322,4 +323,4 @@ function createJiraServer(config = {}, env = process.env) {
|
|
|
322
323
|
};
|
|
323
324
|
}
|
|
324
325
|
|
|
325
|
-
module.exports = { createJiraServer, TrackerError, NoTransitionError };
|
|
326
|
+
module.exports = { createJiraServer, TrackerError, NoTransitionError, CREDENTIAL_ENV: REQUIRED_ENV };
|
package/lib/tracker/youtrack.js
CHANGED
|
@@ -21,16 +21,20 @@
|
|
|
21
21
|
const { TrackerError, NoTransitionError } = require('./jira-server');
|
|
22
22
|
|
|
23
23
|
const REQUIRED_ENV = ['YOUTRACK_URL', 'YOUTRACK_TOKEN'];
|
|
24
|
+
// What `cairn init` tells the user to put in .env — includes the project, which
|
|
25
|
+
// the guard treats as optional only because it can also come from cairn.config.json.
|
|
26
|
+
const CREDENTIAL_ENV = ['YOUTRACK_URL', 'YOUTRACK_TOKEN', 'YOUTRACK_PROJECT'];
|
|
24
27
|
const STAGE_FIELD = 'Stage';
|
|
25
28
|
const ASSIGNEE_FIELD = 'Assignee';
|
|
26
29
|
|
|
27
30
|
function createYouTrack(config = {}, env = process.env) {
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
+
const pfx = config.envPrefix || '';
|
|
32
|
+
const baseUrl = (config.baseUrl || env[pfx + 'YOUTRACK_URL'] || '').replace(/\/+$/, '');
|
|
33
|
+
const token = config.token || env[pfx + 'YOUTRACK_TOKEN'];
|
|
34
|
+
const projectKey = config.projectKey || env[pfx + 'YOUTRACK_PROJECT'];
|
|
31
35
|
|
|
32
36
|
function assertConfig() {
|
|
33
|
-
const missing = REQUIRED_ENV.filter((k, i) => ![baseUrl, token][i]);
|
|
37
|
+
const missing = REQUIRED_ENV.filter((k, i) => ![baseUrl, token][i]).map((k) => pfx + k);
|
|
34
38
|
if (missing.length) {
|
|
35
39
|
throw new TrackerError(
|
|
36
40
|
`Missing YouTrack credentials: ${missing.join(', ')}. Set them in the repo's .env or the environment.`,
|
|
@@ -314,4 +318,4 @@ function createYouTrack(config = {}, env = process.env) {
|
|
|
314
318
|
};
|
|
315
319
|
}
|
|
316
320
|
|
|
317
|
-
module.exports = { createYouTrack, TrackerError, NoTransitionError };
|
|
321
|
+
module.exports = { createYouTrack, TrackerError, NoTransitionError, CREDENTIAL_ENV };
|
package/package.json
CHANGED
package/schema.json
CHANGED
|
@@ -51,6 +51,12 @@
|
|
|
51
51
|
"type": "string",
|
|
52
52
|
"default": ".env",
|
|
53
53
|
"description": "Path, relative to the repo root, of the file holding tracker credentials."
|
|
54
|
+
},
|
|
55
|
+
"envPrefix": {
|
|
56
|
+
"type": "string",
|
|
57
|
+
"default": "",
|
|
58
|
+
"description": "Prepended to every credential var the provider reads (e.g. \"CAIRN_\" → CAIRN_YOUTRACK_TOKEN). Set it only when the repo's own app reads JIRA_*/YOUTRACK_* and the names would collide.",
|
|
59
|
+
"examples": ["CAIRN_"]
|
|
54
60
|
}
|
|
55
61
|
}
|
|
56
62
|
},
|