@artefact-ci/sdd 0.2.0 → 0.2.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 +2 -2
- package/dist/agents.mjs +8 -0
- package/dist/install.mjs +79 -4
- package/dist/report.mjs +11 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -49,7 +49,7 @@ L'installeur enregistre la version de la méthode installée et sa source dans `
|
|
|
49
49
|
|
|
50
50
|
```json
|
|
51
51
|
{
|
|
52
|
-
"version": "0.2.
|
|
52
|
+
"version": "0.2.1",
|
|
53
53
|
"source": "release"
|
|
54
54
|
}
|
|
55
55
|
```
|
|
@@ -64,4 +64,4 @@ Un projet installé avant ce fichier (phase 4, avec `--source`) a déjà les com
|
|
|
64
64
|
|
|
65
65
|
## Ce que `update` touche
|
|
66
66
|
|
|
67
|
-
Il réinstalle l'extension et le preset, puis régénère les fichiers d'agent et réapplique les hooks. Sans `--source`, il les prend dans les zips de la release de sa version : `extension add --from <url du zip> --force` et `preset update artefact-sdd --from <url du zip>`. Avec `--source`, il les prend dans le clone en `--dev` : `extension add --dev … --force` et `preset update artefact-sdd --dev …` (soit un retrait puis un ajout). Si un téléchargement échoue (réseau coupé, version sans release), l'installeur s'arrête en citant l'URL du zip : relancer la même commande une fois le réseau rétabli. `installed_at` change dans les registres de `.specify/` : c'est attendu. Il ne touche jamais `stack.yml` ni la constitution, et s'arrête si `.specify/` est absent.
|
|
67
|
+
Il réinstalle l'extension et le preset, puis régénère les fichiers d'agent et réapplique les hooks. Quand `.specify/integration.json` liste plusieurs outils, il lance `specify integration use` pour chaque outil autre que celui par défaut, puis pour l'outil par défaut : Spec Kit n'enregistre les commandes de l'extension et du preset que pour l'outil sélectionné. L'outil par défaut ne change pas, même si un `use` échoue, et les scripts de `.specify/scripts/` gardent leurs droits. Sans `--source`, il les prend dans les zips de la release de sa version : `extension add --from <url du zip> --force` et `preset update artefact-sdd --from <url du zip>`. Avec `--source`, il les prend dans le clone en `--dev` : `extension add --dev … --force` et `preset update artefact-sdd --dev …` (soit un retrait puis un ajout). Si un téléchargement échoue (réseau coupé, version sans release), l'installeur s'arrête en citant l'URL du zip : relancer la même commande une fois le réseau rétabli. `installed_at` change dans les registres de `.specify/` : c'est attendu. Il ne touche jamais `stack.yml` ni la constitution, et s'arrête si `.specify/` est absent.
|
package/dist/agents.mjs
CHANGED
|
@@ -24,6 +24,14 @@ export function readIntegrations(root) {
|
|
|
24
24
|
return json.installed_integrations;
|
|
25
25
|
return json.integration ? [json.integration] : [];
|
|
26
26
|
}
|
|
27
|
+
/** Intégration par défaut de `.specify/integration.json` (`default_integration`, sinon `integration`) ; null si aucune. */
|
|
28
|
+
export function readDefaultIntegration(root) {
|
|
29
|
+
const file = join(root, '.specify', 'integration.json');
|
|
30
|
+
if (!existsSync(file))
|
|
31
|
+
return null;
|
|
32
|
+
const json = JSON.parse(readFileSync(file, 'utf8'));
|
|
33
|
+
return json.default_integration ?? json.integration ?? null;
|
|
34
|
+
}
|
|
27
35
|
function body(spec) {
|
|
28
36
|
const lines = [
|
|
29
37
|
`Lis \`${INSTRUCTIONS_DIR}/${spec.name}.md\` et applique-le à la lettre : méthode, grille et format du rapport.`,
|
package/dist/install.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
1
|
+
import { chmodSync, existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
2
2
|
import { basename, join } from 'node:path';
|
|
3
|
-
import { agentFiles, readAgentSpecs, readIntegrations, skippedAgents } from "./agents.mjs";
|
|
3
|
+
import { agentFiles, readAgentSpecs, readDefaultIntegration, readIntegrations, skippedAgents } from "./agents.mjs";
|
|
4
4
|
import { compareVersions, readInstalled, writeInstalled } from "./artefact.mjs";
|
|
5
5
|
import { baseConstitution, isBlankConstitution } from "./constitution.mjs";
|
|
6
6
|
import { applyGitHooks } from "./hooks.mjs";
|
|
@@ -62,6 +62,62 @@ async function branches(o) {
|
|
|
62
62
|
const defaultBranch = head?.startsWith('refs/remotes/origin/') ? head.slice('refs/remotes/origin/'.length) : undefined;
|
|
63
63
|
return { names: [...new Set(refs)], ...(defaultBranch ? { defaultBranch } : {}) };
|
|
64
64
|
}
|
|
65
|
+
const SCRIPTS = '.specify/scripts';
|
|
66
|
+
/** Droits de chaque fichier sous `.specify/scripts/` (chemin absolu → mode). */
|
|
67
|
+
function scriptModes(root) {
|
|
68
|
+
const modes = new Map();
|
|
69
|
+
const walk = (dir) => {
|
|
70
|
+
if (!existsSync(dir))
|
|
71
|
+
return;
|
|
72
|
+
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
73
|
+
const p = join(dir, e.name);
|
|
74
|
+
if (e.isDirectory())
|
|
75
|
+
walk(p);
|
|
76
|
+
else if (e.isFile())
|
|
77
|
+
modes.set(p, statSync(p).mode & 0o777);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
walk(join(root, SCRIPTS));
|
|
81
|
+
return modes;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Spec Kit 1.3.2 n'enregistre les commandes d'une extension ou d'un preset que pour l'intégration
|
|
85
|
+
* par défaut ; une autre ne les reçoit qu'à `specify integration use`. On sélectionne donc chaque
|
|
86
|
+
* intégration secondaire, puis on revient toujours à celle par défaut, même après un échec.
|
|
87
|
+
* `use` réécrit les scripts partagés sans leur droit d'exécution : on remet les droits d'avant.
|
|
88
|
+
*/
|
|
89
|
+
async function registerSecondary(o, integrations) {
|
|
90
|
+
const fallback = readDefaultIntegration(o.root);
|
|
91
|
+
if (fallback === null || !integrations.includes(fallback))
|
|
92
|
+
return [];
|
|
93
|
+
const others = integrations.filter((i) => i !== fallback);
|
|
94
|
+
if (others.length === 0)
|
|
95
|
+
return [];
|
|
96
|
+
const modes = scriptModes(o.root);
|
|
97
|
+
let failed;
|
|
98
|
+
for (const integration of others) {
|
|
99
|
+
try {
|
|
100
|
+
await specify(o.runner, o.root, ['integration', 'use', integration]);
|
|
101
|
+
}
|
|
102
|
+
catch (e) {
|
|
103
|
+
failed = { integration, error: e };
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
await specify(o.runner, o.root, ['integration', 'use', fallback]);
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
for (const [file, mode] of modes) {
|
|
112
|
+
if (existsSync(file) && (statSync(file).mode & 0o777) !== mode)
|
|
113
|
+
chmodSync(file, mode);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (failed) {
|
|
117
|
+
throw new SetupError(`impossible d'enregistrer les commandes de la méthode pour ${failed.integration} : ${failed.error.message} ; ${fallback} reste l'outil par défaut, relance update`);
|
|
118
|
+
}
|
|
119
|
+
return others;
|
|
120
|
+
}
|
|
65
121
|
async function isDirty(o) {
|
|
66
122
|
return ((await git(o, ['status', '--porcelain'])) ?? '').trim() !== '';
|
|
67
123
|
}
|
|
@@ -101,6 +157,8 @@ async function common(o, mode) {
|
|
|
101
157
|
catch (e) {
|
|
102
158
|
throw new SetupError(`.specify/integration.json illisible : ${e.message}`);
|
|
103
159
|
}
|
|
160
|
+
// Seulement quand un composant vient d'être (ré)installé : un second init ne lance rien.
|
|
161
|
+
const registeredFor = installed.length > 0 ? await registerSecondary(o, integrations) : [];
|
|
104
162
|
const specs = readAgentSpecs(root);
|
|
105
163
|
const agents = [];
|
|
106
164
|
const agentsSkipped = [];
|
|
@@ -127,6 +185,7 @@ async function common(o, mode) {
|
|
|
127
185
|
agents,
|
|
128
186
|
agentsSkipped,
|
|
129
187
|
noAgentGenerator,
|
|
188
|
+
registeredFor,
|
|
130
189
|
legacy: legacyAgents(root),
|
|
131
190
|
};
|
|
132
191
|
}
|
|
@@ -141,7 +200,8 @@ export async function init(o) {
|
|
|
141
200
|
}
|
|
142
201
|
const noneInstalled = !registered(root, 'presets', 'artefact-sdd') && !registered(root, 'extensions', 'artefact-product');
|
|
143
202
|
const dirty = await isDirty(o);
|
|
144
|
-
|
|
203
|
+
const hadSpecify = existsSync(join(root, '.specify'));
|
|
204
|
+
if (!hadSpecify) {
|
|
145
205
|
const args = ['init', '--here', '--script', 'sh', '--non-interactive', '--ignore-agent-tools'];
|
|
146
206
|
args.push('--integration', o.integration ?? 'claude');
|
|
147
207
|
// Spec Kit 1.3.2 compte `.git` comme contenu : sans --force, il refuse un dossier non vide.
|
|
@@ -169,7 +229,22 @@ export async function init(o) {
|
|
|
169
229
|
writeIfChanged(constFile, baseConstitution(readFileSync(template, 'utf8'), basename(root)));
|
|
170
230
|
constitution = 'written';
|
|
171
231
|
}
|
|
172
|
-
|
|
232
|
+
// Spec Kit n'installe une intégration qu'à son init : sur un projet déjà initialisé, --integration ne fait rien.
|
|
233
|
+
let integrationIgnored;
|
|
234
|
+
if (hadSpecify && o.integration !== undefined) {
|
|
235
|
+
const installed = readIntegrations(root);
|
|
236
|
+
if (!installed.includes(o.integration))
|
|
237
|
+
integrationIgnored = { requested: o.integration, installed };
|
|
238
|
+
}
|
|
239
|
+
return {
|
|
240
|
+
mode: 'init',
|
|
241
|
+
...done,
|
|
242
|
+
stack,
|
|
243
|
+
constitution,
|
|
244
|
+
dirty,
|
|
245
|
+
...(integrationIgnored ? { integrationIgnored } : {}),
|
|
246
|
+
version: { installed: record ? current : null, previous, downgrade: false }
|
|
247
|
+
};
|
|
173
248
|
}
|
|
174
249
|
export async function update(o) {
|
|
175
250
|
if (!(await hasUv(o.runner)))
|
package/dist/report.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SPECIFY_VERSION } from "./specify.mjs";
|
|
1
2
|
const LEGACY_STATE = {
|
|
2
3
|
unchanged: 'identique au boilerplate, tu peux le supprimer',
|
|
3
4
|
/** Variante quand des fichiers citent encore docs/agents/. */
|
|
@@ -17,6 +18,9 @@ export function formatReport(r) {
|
|
|
17
18
|
}
|
|
18
19
|
if (r.skipped.length > 0)
|
|
19
20
|
out.push(` Déjà installé : ${list(r.skipped)}`);
|
|
21
|
+
if (r.registeredFor.length > 0) {
|
|
22
|
+
out.push(` Commandes de la méthode enregistrées aussi pour : ${list(r.registeredFor)} (outil par défaut inchangé)`);
|
|
23
|
+
}
|
|
20
24
|
const { installed, previous, downgrade } = r.version;
|
|
21
25
|
if (installed === null) {
|
|
22
26
|
out.push(' Version installée inconnue : lance update pour aligner tous les composants');
|
|
@@ -50,7 +54,7 @@ export function formatReport(r) {
|
|
|
50
54
|
out.push(` ${name} : pas de générateur de fichiers d'agent, la relecture se fait en contexte neuf`);
|
|
51
55
|
}
|
|
52
56
|
for (const { integration, names } of r.agentsSkipped) {
|
|
53
|
-
out.push(` ${integration}
|
|
57
|
+
out.push(` ${integration} : pas de fichier d'agent ${list(names)} pour cet outil : la commande fera ce travail en nouvelle session`);
|
|
54
58
|
}
|
|
55
59
|
}
|
|
56
60
|
if (r.stack) {
|
|
@@ -74,6 +78,12 @@ export function formatReport(r) {
|
|
|
74
78
|
out.push(` Citent encore docs/agents/ : ${list(r.legacy.mentions)}`);
|
|
75
79
|
}
|
|
76
80
|
}
|
|
81
|
+
if (r.integrationIgnored) {
|
|
82
|
+
const { requested, installed } = r.integrationIgnored;
|
|
83
|
+
const current = installed.length > 0 ? list(installed) : 'aucune intégration';
|
|
84
|
+
const specify = `uvx --from specify-cli==${SPECIFY_VERSION} specify integration`;
|
|
85
|
+
out.push('', `Attention : --integration ${requested} ignoré : Spec Kit était déjà installé ici avec ${current}.`, ` pour garder ${current} et ajouter ${requested} : ${specify} install ${requested} --script sh --force`, ` pour remplacer ${current} par ${requested} : ${specify} switch ${requested}`, " puis : npx @artefact-ci/sdd update (voir « Changer ou ajouter un outil » dans le README de speckit-artefact)");
|
|
86
|
+
}
|
|
77
87
|
if (r.dirty) {
|
|
78
88
|
out.push('', "Attention : le dépôt avait des changements non commités avant l'installation ; relis le diff avant de commiter.");
|
|
79
89
|
}
|