@timekast/factory 1.0.0 → 1.1.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/dist/commands/doctor.js +2 -24
- package/dist/commands/update.js +2 -25
- package/dist/lib/claude-paths.js +68 -0
- package/dist/lib/constants.js +16 -1
- package/dist/lib/package-json.js +41 -26
- package/package.json +1 -1
package/dist/commands/doctor.js
CHANGED
|
@@ -16,8 +16,9 @@
|
|
|
16
16
|
* No lockfile (pre-CLI repo) → delegate to the "run factory:update" advice, exit
|
|
17
17
|
* 0, never abort (same pattern as `status` / the §8 edge case).
|
|
18
18
|
*/
|
|
19
|
-
import { existsSync, readFileSync,
|
|
19
|
+
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
20
20
|
import path from 'node:path';
|
|
21
|
+
import { collectClaudePaths } from '../lib/claude-paths.js';
|
|
21
22
|
import { CLAUDE_MD_FILE } from '../lib/constants.js';
|
|
22
23
|
import { hasLockfile, normalizeThenHash, readLockfile } from '../lib/lockfile.js';
|
|
23
24
|
import { detectRepo } from '../lib/repo-detection.js';
|
|
@@ -42,29 +43,6 @@ export const A1_WARNING = 'Aviso de seguridad (A1): el boilerplate `src/` y sus
|
|
|
42
43
|
'dependencia) NO llega por este canal. El canal de parches de `src/` es EPIC 2 ' +
|
|
43
44
|
'(update agentico con merge inteligente), aún no disponible. Mantén `src/` y tus ' +
|
|
44
45
|
'dependencias al día por tu cuenta mientras tanto.';
|
|
45
|
-
/**
|
|
46
|
-
* Recursively collect repo-relative POSIX paths of files under `.claude/`. Only
|
|
47
|
-
* descends into `.claude/`; `src/` and everything else is invisible (design
|
|
48
|
-
* §7.2). Mirrors the `update` engine's `collectClaudePaths`.
|
|
49
|
-
*/
|
|
50
|
-
function collectClaudePaths(rootDir) {
|
|
51
|
-
const out = [];
|
|
52
|
-
const walk = (rel) => {
|
|
53
|
-
const abs = path.join(rootDir, rel);
|
|
54
|
-
if (!existsSync(abs))
|
|
55
|
-
return;
|
|
56
|
-
if (statSync(abs).isDirectory()) {
|
|
57
|
-
for (const entry of readdirSync(abs)) {
|
|
58
|
-
walk(path.posix.join(rel, entry));
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
else {
|
|
62
|
-
out.push(rel);
|
|
63
|
-
}
|
|
64
|
-
};
|
|
65
|
-
walk('.claude');
|
|
66
|
-
return out;
|
|
67
|
-
}
|
|
68
46
|
/**
|
|
69
47
|
* Concatenate the body of the reserved "on-demand reference" section of CLAUDE.md
|
|
70
48
|
* — the lines under a heading matching `ON_DEMAND_HEADING_RE`, until the next
|
package/dist/commands/update.js
CHANGED
|
@@ -33,11 +33,12 @@
|
|
|
33
33
|
* `update` NEVER touches `src/` — files outside the lockfile/manifest are
|
|
34
34
|
* invisible (design §9 out-of-scope).
|
|
35
35
|
*/
|
|
36
|
-
import { existsSync, mkdtempSync, readFileSync,
|
|
36
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync, statSync } from 'node:fs';
|
|
37
37
|
import { tmpdir } from 'node:os';
|
|
38
38
|
import path from 'node:path';
|
|
39
39
|
import prompts from 'prompts';
|
|
40
40
|
import { applyPlan, clearUpdateState, defaultBackupDir, hasUpdateState, readUpdateState, validateStagedManifest, writeUpdateState, } from '../lib/atomic-swap.js';
|
|
41
|
+
import { collectClaudePaths } from '../lib/claude-paths.js';
|
|
41
42
|
import { CLIError } from '../lib/cli-error.js';
|
|
42
43
|
import { CLAUDE_MD_FILE, PROFILES } from '../lib/constants.js';
|
|
43
44
|
import { diffLockfiles, hasLockfile, normalizeThenHash, planAutoRegister, readLockfile, writeLockfile, } from '../lib/lockfile.js';
|
|
@@ -64,30 +65,6 @@ async function acquireFromRelease(profile) {
|
|
|
64
65
|
const manifest = validateStagedManifest(stagedDir, manifestRaw);
|
|
65
66
|
return { stagedDir, manifest, tmpRoot };
|
|
66
67
|
}
|
|
67
|
-
/**
|
|
68
|
-
* Recursively collect repo-relative paths of files under `.claude/` plus any
|
|
69
|
-
* tracked root files the manifest references at the top level. Used both for
|
|
70
|
-
* disk hashing and auto-register path-match. Only descends into `.claude/`;
|
|
71
|
-
* `src/` and everything else is invisible (design §7.2).
|
|
72
|
-
*/
|
|
73
|
-
function collectClaudePaths(rootDir) {
|
|
74
|
-
const out = [];
|
|
75
|
-
const walk = (rel) => {
|
|
76
|
-
const abs = path.join(rootDir, rel);
|
|
77
|
-
if (!existsSync(abs))
|
|
78
|
-
return;
|
|
79
|
-
if (statSync(abs).isDirectory()) {
|
|
80
|
-
for (const entry of readdirSync(abs)) {
|
|
81
|
-
walk(path.posix.join(rel, entry));
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
else {
|
|
85
|
-
out.push(rel);
|
|
86
|
-
}
|
|
87
|
-
};
|
|
88
|
-
walk('.claude');
|
|
89
|
-
return out;
|
|
90
|
-
}
|
|
91
68
|
/**
|
|
92
69
|
* Hash the disk content (normalized) of every path in `paths` that exists.
|
|
93
70
|
* A missing file is simply absent from the returned map.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collect the repo-relative POSIX paths of files under `.claude/`, EXCLUDING
|
|
3
|
+
* gitignored ones. Shared by `update` (legacy auto-register ambiguous report)
|
|
4
|
+
* and `doctor` (orphan report) so neither flags a dev-local, gitignored file
|
|
5
|
+
* (`settings.local.json`, `transitions/`, `.DS_Store`) as "ambiguous"/"orphan" —
|
|
6
|
+
* those are never kit-managed, so surfacing them is pure noise.
|
|
7
|
+
*
|
|
8
|
+
* Only descends into `.claude/`; `src/` and everything else is invisible
|
|
9
|
+
* (design §7.2).
|
|
10
|
+
*/
|
|
11
|
+
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import { spawnSync } from 'node:child_process';
|
|
14
|
+
/**
|
|
15
|
+
* Fallback ignore set, used only when `git check-ignore` is unavailable (no git
|
|
16
|
+
* binary, or the dir is not a git repo). Mirrors the kit's own `.gitignore`
|
|
17
|
+
* entries for `.claude/`. The git path is authoritative when present.
|
|
18
|
+
*/
|
|
19
|
+
const IGNORE_FALLBACK_RE = /(^|\/)(\.DS_Store|settings\.local\.json)$|(^|\/)transitions\//;
|
|
20
|
+
/** Recursively gather repo-relative POSIX paths of files under `.claude/`. */
|
|
21
|
+
function walkClaude(rootDir) {
|
|
22
|
+
const out = [];
|
|
23
|
+
const walk = (rel) => {
|
|
24
|
+
const abs = path.join(rootDir, rel);
|
|
25
|
+
if (!existsSync(abs))
|
|
26
|
+
return;
|
|
27
|
+
if (statSync(abs).isDirectory()) {
|
|
28
|
+
for (const entry of readdirSync(abs))
|
|
29
|
+
walk(path.posix.join(rel, entry));
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
out.push(rel);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
walk('.claude');
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Drop the gitignored paths from `paths`. Uses `git check-ignore --stdin` (the
|
|
40
|
+
* authoritative source — respects nested `.gitignore`, negations, etc.); falls
|
|
41
|
+
* back to `IGNORE_FALLBACK_RE` when git is missing or the dir is not a repo.
|
|
42
|
+
*
|
|
43
|
+
* `git check-ignore` exit codes: 0 = some paths matched (printed), 1 = none
|
|
44
|
+
* matched (empty output), 128 = error / not a repo. `spawnSync` does not throw,
|
|
45
|
+
* so 0 and 1 both yield usable stdout; >1 or a spawn error → fallback.
|
|
46
|
+
*/
|
|
47
|
+
function dropGitignored(rootDir, paths) {
|
|
48
|
+
if (paths.length === 0)
|
|
49
|
+
return paths;
|
|
50
|
+
const res = spawnSync('git', ['-C', rootDir, 'check-ignore', '--stdin'], {
|
|
51
|
+
input: paths.join('\n'),
|
|
52
|
+
encoding: 'utf8',
|
|
53
|
+
});
|
|
54
|
+
if (res.error || res.status === null || res.status > 1) {
|
|
55
|
+
return paths.filter((p) => !IGNORE_FALLBACK_RE.test(p));
|
|
56
|
+
}
|
|
57
|
+
const ignored = new Set((res.stdout || '')
|
|
58
|
+
.split('\n')
|
|
59
|
+
.map((l) => l.trim())
|
|
60
|
+
.filter(Boolean));
|
|
61
|
+
return paths.filter((p) => !ignored.has(p));
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Repo-relative POSIX paths of NON-gitignored files under `.claude/`.
|
|
65
|
+
*/
|
|
66
|
+
export function collectClaudePaths(rootDir) {
|
|
67
|
+
return dropGitignored(rootDir, walkClaude(rootDir));
|
|
68
|
+
}
|
package/dist/lib/constants.js
CHANGED
|
@@ -26,9 +26,24 @@ export const LOCKFILE_FILE = 'lockfile.json';
|
|
|
26
26
|
* exists on disk (path-match path). See `diffLockfiles` + the install commands.
|
|
27
27
|
*/
|
|
28
28
|
export const CLAUDE_MD_FILE = 'CLAUDE.md';
|
|
29
|
-
/** The
|
|
29
|
+
/** The scripts the CLI injects into a derived project's package.json. */
|
|
30
30
|
export const UPDATE_SCRIPT_NAME = 'factory:update';
|
|
31
31
|
// `npx` so the script resolves in a fresh derived repo where @timekast/factory
|
|
32
32
|
// is NOT a dependency (B3): a bare `@timekast/factory update` would be
|
|
33
33
|
// "command not found". npx resolves the published package on demand.
|
|
34
34
|
export const UPDATE_SCRIPT_CMD = 'npx @timekast/factory update';
|
|
35
|
+
export const DOCTOR_SCRIPT_NAME = 'factory:doctor';
|
|
36
|
+
export const DOCTOR_SCRIPT_CMD = 'npx @timekast/factory doctor';
|
|
37
|
+
export const STATUS_SCRIPT_NAME = 'factory:status';
|
|
38
|
+
export const STATUS_SCRIPT_CMD = 'npx @timekast/factory status';
|
|
39
|
+
/**
|
|
40
|
+
* All convenience scripts the installer ensures in a Node derivative's
|
|
41
|
+
* package.json (each: add if missing, never overwrite a divergent value). Keyed
|
|
42
|
+
* by script name → command. `factory:update` is the primary (drives the install
|
|
43
|
+
* messaging); `doctor`/`status` are read-only diagnostics with no other alias.
|
|
44
|
+
*/
|
|
45
|
+
export const FACTORY_SCRIPTS = {
|
|
46
|
+
[UPDATE_SCRIPT_NAME]: UPDATE_SCRIPT_CMD,
|
|
47
|
+
[DOCTOR_SCRIPT_NAME]: DOCTOR_SCRIPT_CMD,
|
|
48
|
+
[STATUS_SCRIPT_NAME]: STATUS_SCRIPT_CMD,
|
|
49
|
+
};
|
package/dist/lib/package-json.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
import { CLIError } from './cli-error.js';
|
|
11
|
-
import {
|
|
11
|
+
import { FACTORY_SCRIPTS, PROFILES, UPDATE_SCRIPT_NAME } from './constants.js';
|
|
12
12
|
/**
|
|
13
13
|
* Auto-detect the profile to install into an existing repo from its
|
|
14
14
|
* `package.json`: a `factoryVersion` field means the repo was born from the
|
|
@@ -58,26 +58,40 @@ export function parsePackageJson(raw) {
|
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
60
|
/**
|
|
61
|
-
* Pure core of the script insertion: mutate `pkg.scripts` in place to ensure
|
|
62
|
-
* `factory
|
|
61
|
+
* Pure core of the script insertion: mutate `pkg.scripts` in place to ensure ALL
|
|
62
|
+
* `factory:*` convenience scripts exist (`update` / `doctor` / `status`), adding
|
|
63
|
+
* each if missing and NEVER overwriting a divergent value (§7.4). Shared by
|
|
63
64
|
* `applyPackageJsonEdits` (the `new` flow) and `insertFactoryUpdateScript`
|
|
64
|
-
* (the `update` flow)
|
|
65
|
+
* (the `update` flow). Returns whether anything changed (so the caller writes
|
|
66
|
+
* only on a real edit) + the result for the primary `factory:update` script
|
|
67
|
+
* (which drives the install messaging).
|
|
65
68
|
*/
|
|
66
|
-
function
|
|
69
|
+
function ensureFactoryScripts(pkg) {
|
|
67
70
|
const scripts = pkg.scripts ?? {};
|
|
68
|
-
const existing = scripts[UPDATE_SCRIPT_NAME];
|
|
69
71
|
pkg.scripts = scripts;
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
72
|
+
let changed = false;
|
|
73
|
+
let primary = { action: 'already-correct' };
|
|
74
|
+
for (const [name, cmd] of Object.entries(FACTORY_SCRIPTS)) {
|
|
75
|
+
const existing = scripts[name];
|
|
76
|
+
let result;
|
|
77
|
+
if (existing === undefined) {
|
|
78
|
+
scripts[name] = cmd;
|
|
79
|
+
changed = true;
|
|
80
|
+
result = { action: 'added' };
|
|
81
|
+
}
|
|
82
|
+
else if (existing === cmd) {
|
|
83
|
+
result = { action: 'already-correct' };
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
result = { action: 'conflict', existingValue: existing };
|
|
87
|
+
}
|
|
88
|
+
if (name === UPDATE_SCRIPT_NAME)
|
|
89
|
+
primary = result;
|
|
76
90
|
}
|
|
77
|
-
return {
|
|
91
|
+
return { changed, primary };
|
|
78
92
|
}
|
|
79
93
|
/**
|
|
80
|
-
* Rename `package.json.name` and ensure the `factory
|
|
94
|
+
* Rename `package.json.name` and ensure the `factory:*` scripts exist,
|
|
81
95
|
* preserving everything else. Returns the serialized document (with the
|
|
82
96
|
* original indentation + trailing newline).
|
|
83
97
|
*
|
|
@@ -88,31 +102,32 @@ export function applyPackageJsonEdits(raw, name) {
|
|
|
88
102
|
const indent = detectIndent(raw);
|
|
89
103
|
const pkg = parsePackageJson(raw);
|
|
90
104
|
pkg.name = name;
|
|
91
|
-
const
|
|
105
|
+
const { primary } = ensureFactoryScripts(pkg);
|
|
92
106
|
const content = `${JSON.stringify(pkg, null, indent)}\n`;
|
|
93
|
-
return { content, scriptAlreadyPresent:
|
|
107
|
+
return { content, scriptAlreadyPresent: primary.action === 'conflict' };
|
|
94
108
|
}
|
|
95
109
|
/**
|
|
96
|
-
* Surgically ensure the `factory
|
|
97
|
-
* `pkgPath` (design §7.4). Reads, mutates only
|
|
98
|
-
* with the original indentation + trailing
|
|
99
|
-
* value and NEVER touches `name`, deps,
|
|
110
|
+
* Surgically ensure the `factory:*` scripts (`update` / `doctor` / `status`)
|
|
111
|
+
* exist in the `package.json` at `pkgPath` (design §7.4). Reads, mutates only
|
|
112
|
+
* those keys, and re-serializes with the original indentation + trailing
|
|
113
|
+
* newline. NEVER overwrites a divergent value and NEVER touches `name`, deps,
|
|
114
|
+
* or any other script.
|
|
100
115
|
*
|
|
101
|
-
* Writes the file only when
|
|
102
|
-
*
|
|
116
|
+
* Writes the file only when at least one script was added; if all are already
|
|
117
|
+
* present (correct or divergent) the file is left byte-identical.
|
|
103
118
|
*
|
|
104
119
|
* @param pkgPath Absolute path to the derived project's `package.json`.
|
|
105
|
-
* @returns
|
|
120
|
+
* @returns The result for the primary `factory:update` script (+ existing value on conflict).
|
|
106
121
|
*/
|
|
107
122
|
export function insertFactoryUpdateScript(pkgPath) {
|
|
108
123
|
const raw = readFileSync(pkgPath, 'utf8');
|
|
109
124
|
const indent = detectIndent(raw);
|
|
110
125
|
const pkg = parsePackageJson(raw);
|
|
111
|
-
const
|
|
112
|
-
if (
|
|
126
|
+
const { changed, primary } = ensureFactoryScripts(pkg);
|
|
127
|
+
if (changed) {
|
|
113
128
|
writeFileSync(pkgPath, `${JSON.stringify(pkg, null, indent)}\n`, 'utf8');
|
|
114
129
|
}
|
|
115
|
-
return
|
|
130
|
+
return primary;
|
|
116
131
|
}
|
|
117
132
|
/**
|
|
118
133
|
* Surgically set the derived project's `package.json.agentKitVersion` to the
|