@timekast/factory 1.10.1 → 1.11.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.
@@ -0,0 +1,43 @@
1
+ /**
2
+ * `factory bootstrap-version` — report / compare the bootstrap version (BOOT-002,
3
+ * consumed by BOOT-003). The macOS launcher bootstrap is a tiny stable `.app`;
4
+ * when its logic changes we publish a new `launcher-v*` bootstrap. This command
5
+ * lets an installed bootstrap ask the CLI "am I outdated?" without baking the
6
+ * latest version number into the `.app`.
7
+ *
8
+ * Contract BOOT-003 consumes:
9
+ * factory bootstrap-version --have <version> → prints `outdated` (have < expected)
10
+ * or `ok` (have >= expected)
11
+ * factory bootstrap-version → prints EXPECTED_BOOTSTRAP_VERSION
12
+ * (debug aid)
13
+ *
14
+ * The comparison goes through `compare()` from `lib/semver.ts` — the existing
15
+ * total comparator — NOT a naive string compare (so `1.10.0` > `1.9.0`).
16
+ */
17
+ import { compare } from '../lib/semver.js';
18
+ /**
19
+ * SSOT for the latest published bootstrap version. Bump this when a new
20
+ * `launcher-v*` bootstrap ships so installed launchers learn they are outdated.
21
+ */
22
+ export const EXPECTED_BOOTSTRAP_VERSION = '1.0.0';
23
+ /** Read the value of a `--have <version>` flag from the arg list, if present. */
24
+ function parseHave(args) {
25
+ const idx = args.indexOf('--have');
26
+ if (idx === -1)
27
+ return undefined;
28
+ return args[idx + 1];
29
+ }
30
+ /**
31
+ * Run `factory bootstrap-version`. With `--have <version>`, compares it against
32
+ * EXPECTED_BOOTSTRAP_VERSION and prints `outdated` / `ok`; without it, prints
33
+ * the expected version.
34
+ */
35
+ export function runBootstrapVersion(args) {
36
+ const have = parseHave(args);
37
+ if (have === undefined) {
38
+ console.log(EXPECTED_BOOTSTRAP_VERSION);
39
+ return;
40
+ }
41
+ // have < expected → outdated; equal or newer → ok.
42
+ console.log(compare(have, EXPECTED_BOOTSTRAP_VERSION) < 0 ? 'outdated' : 'ok');
43
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * `factory launcher-path` — print the absolute path to the packaged launcher
3
+ * AppleScript (BOOT-002). This is the CLI side of the launcher auto-update: the
4
+ * macOS bootstrap (BOOT-001) resolves `@timekast/factory@latest`, runs this
5
+ * command to discover where the GUI logic lives inside the installed package,
6
+ * and `run script`s it. So shipping a new `cli-v*` updates the launcher logic
7
+ * without recompiling or redistributing the `.app`.
8
+ *
9
+ * Path resolution is MODULE-RELATIVE, never `process.cwd()`: npx runs from a
10
+ * temporary cache directory, so `cwd` would point at the user's project, not at
11
+ * the installed package. At runtime this module lives at `dist/commands/`, so
12
+ * `..` lands on `dist/`, where the build copy-step places `launcher.applescript`
13
+ * (see the `build` script in `cli/package.json` + `files: ["dist"]`).
14
+ *
15
+ * We ship the SOURCE `.applescript` (not a compiled `.scpt`): it is legible and
16
+ * diffeable in git, and `run script` accepts a source script just fine. The few
17
+ * milliseconds a compiled script would save do not justify shipping an opaque
18
+ * binary blob in the package.
19
+ */
20
+ import { dirname, join } from 'node:path';
21
+ import { fileURLToPath } from 'node:url';
22
+ /** Resolve the absolute path to the packaged `launcher.applescript`, relative to this module. */
23
+ export function launcherPath() {
24
+ const here = dirname(fileURLToPath(import.meta.url));
25
+ // dist/commands/launcher-path.js → dist/launcher.applescript
26
+ return join(here, '..', 'launcher.applescript');
27
+ }
28
+ /** Print the absolute path of the packaged launcher script to stdout. */
29
+ export function runLauncherPath() {
30
+ console.log(launcherPath());
31
+ }
package/dist/index.js CHANGED
@@ -11,7 +11,9 @@ import { dirname, join } from 'node:path';
11
11
  import { fileURLToPath } from 'node:url';
12
12
  import { CLIError } from './lib/cli-error.js';
13
13
  import { parseAddFlags, runAdd } from './commands/add.js';
14
+ import { runBootstrapVersion } from './commands/bootstrap-version.js';
14
15
  import { runDoctor } from './commands/doctor.js';
16
+ import { runLauncherPath } from './commands/launcher-path.js';
15
17
  import { parseNewFlags, runNew } from './commands/new.js';
16
18
  import { parsePublishArgs, runPublish } from './commands/publish.js';
17
19
  import { secretsCommand } from './commands/secrets.js';
@@ -34,6 +36,8 @@ Comandos:
34
36
  publish <qué> Publica el entregable (proposal|mockup|both) a proposals.timekast.mx
35
37
  unpublish [qué] Baja del hub un entregable publicado (proposal|mockup|both; default both)
36
38
  secrets <file> Mergea un archivo KEY=value de secretos de metodología a ~/.claude/.env (chmod 600)
39
+ launcher-path (interno) Imprime el path del launcher.applescript empacado (lo usa el bootstrap)
40
+ bootstrap-version (interno) Imprime/compara la versión del bootstrap (\`--have <ver>\` → ok|outdated)
37
41
 
38
42
  Sobre \`new\`:
39
43
  Pregunta el perfil de forma interactiva. Los flags \`--full\` / \`--core\` lo
@@ -147,6 +151,14 @@ async function main(argv) {
147
151
  await secretsCommand(file);
148
152
  return;
149
153
  }
154
+ case 'launcher-path': {
155
+ runLauncherPath();
156
+ return;
157
+ }
158
+ case 'bootstrap-version': {
159
+ runBootstrapVersion(rest);
160
+ return;
161
+ }
150
162
  default:
151
163
  throw new CLIError(`Comando desconocido: \`${command}\`.\nEjecuta \`factory --help\` para ver los comandos disponibles.`);
152
164
  }
@@ -0,0 +1,239 @@
1
+ -- TimeKast Factory — launcher (macOS)
2
+ -- Crea proyectos nuevos y actualiza proyectos existentes SIN abrir la terminal.
3
+ -- Envuelve `npx @timekast/factory`. Se ejecuta dentro de un login shell
4
+ -- (`${SHELL:-/bin/zsh} -lc`) para heredar el PATH real del usuario (node / npx /
5
+ -- gh por nvm o homebrew no están en el PATH mínimo de `do shell script`). Usa
6
+ -- `$SHELL` (que `do shell script` sí exporta) con fallback a /bin/zsh; soporte
7
+ -- solo para shells POSIX compatibles con `-lc` (zsh/bash).
8
+ --
9
+ -- Requisitos (una sola vez por máquina): Node 22+, `gh auth login` hecho,
10
+ -- miembro de la org TimeKast en GitHub.
11
+
12
+ property kCLIPkg : "@timekast/factory"
13
+ property kCLI : "@timekast/factory@latest" -- default (fallback); `on run` lo resuelve vía resolvedCLI (pin opcional)
14
+ property kTitle : "TimeKast Factory"
15
+ property kOrg : "TimeKast"
16
+
17
+ on run
18
+ -- Resuelve la versión del CLI a usar (pin opcional sin recompilar; ver `resolvedCLI`).
19
+ -- Se reasigna la property global kCLI para que los 3 call-sites la usen sin cambios.
20
+ set kCLI to resolvedCLI()
21
+
22
+ set homeRoot to (POSIX path of (path to home folder)) & "timekast"
23
+ do shell script "mkdir -p " & quoted form of homeRoot
24
+
25
+ if not checkPrereqs() then return
26
+
27
+ set theAction to choose from list {"Nuevo proyecto", "Actualizar proyecto", "Poner secrets"} with prompt "TimeKast Factory — ¿qué quieres hacer?" default items {"Nuevo proyecto"} OK button name "Continuar" cancel button name "Salir"
28
+ if theAction is false then return
29
+ set theAction to item 1 of theAction
30
+
31
+ if theAction is "Nuevo proyecto" then
32
+ newProject(homeRoot)
33
+ else if theAction is "Actualizar proyecto" then
34
+ updateProject(homeRoot)
35
+ else
36
+ installSecrets()
37
+ end if
38
+ end run
39
+
40
+ -- ───────────────────── Versión del CLI (pin opcional) ─────────────────────
41
+ -- Por defecto usa `@timekast/factory@latest`. Para fijar una versión SIN recompilar
42
+ -- la .app, crea `~/.timekast-launcher.conf` con una línea: CLI_VERSION=@1.9.0
43
+ -- (también acepta `CLI_VERSION=1.9.0`). Útil para validar una versión específica antes
44
+ -- de mover al equipo a @latest. Sin archivo o sin la línea → fallback a kCLI (@latest).
45
+ on resolvedCLI()
46
+ try
47
+ set confPath to (POSIX path of (path to home folder)) & ".timekast-launcher.conf"
48
+ set pinned to do shell script "grep -E '^CLI_VERSION=' " & quoted form of confPath & " 2>/dev/null | tail -1 | cut -d= -f2- | tr -d '[:space:]'"
49
+ if pinned is not "" then
50
+ if pinned starts with "@" then
51
+ return kCLIPkg & pinned
52
+ else
53
+ return kCLIPkg & "@" & pinned
54
+ end if
55
+ end if
56
+ end try
57
+ return kCLI
58
+ end resolvedCLI
59
+
60
+ -- ───────────────────── Preflight de requisitos ─────────────────────
61
+ -- true si node + gh + gh-auth están listos; si no, avisa cómo arreglarlo y
62
+ -- devuelve false. Corre en login shell (`${SHELL:-/bin/zsh} -lc`) para ver el PATH real.
63
+ on checkPrereqs()
64
+ try
65
+ do shell script "${SHELL:-/bin/zsh} -lc 'command -v node'"
66
+ on error
67
+ display dialog "Falta Node.js." & return & return & "Instálalo y vuelve a abrir la app:" & return & " https://nodejs.org (o: brew install node)" with title kTitle buttons {"Entendido"} default button "Entendido" with icon stop
68
+ return false
69
+ end try
70
+ -- Versión de Node: el CLI requiere Node 22+. El `22` va hardcodeado: boundary
71
+ -- AppleScript — no puede importar la constante de `engines.node` del CLI TypeScript.
72
+ -- Si sube el mínimo, actualizar aquí y en cli/launchers/README.md (Requisitos).
73
+ try
74
+ set nodeMajor to (do shell script "${SHELL:-/bin/zsh} -lc 'node -v' | sed 's/^v//' | cut -d. -f1") as integer
75
+ if nodeMajor < 22 then
76
+ display dialog "Necesitas Node 22 o superior." & return & return & "Tienes la versión " & nodeMajor & "." & return & return & "Instala una versión nueva desde:" & return & " https://nodejs.org" with title kTitle buttons {"Entendido"} default button "Entendido" with icon stop
77
+ return false
78
+ end if
79
+ on error
80
+ display dialog "No pude verificar la versión de Node.js." & return & return & "Asegúrate de tener Node 22 o superior:" & return & " https://nodejs.org" with title kTitle buttons {"Entendido"} default button "Entendido" with icon stop
81
+ return false
82
+ end try
83
+ try
84
+ do shell script "${SHELL:-/bin/zsh} -lc 'command -v gh'"
85
+ on error
86
+ display dialog "Falta GitHub CLI (gh)." & return & return & "Instálalo y vuelve a abrir la app:" & return & " brew install gh" with title kTitle buttons {"Entendido"} default button "Entendido" with icon stop
87
+ return false
88
+ end try
89
+ try
90
+ do shell script "${SHELL:-/bin/zsh} -lc 'gh auth status'"
91
+ on error
92
+ display dialog "GitHub CLI no está autenticado." & return & return & "Inicia sesión una sola vez en la terminal:" & return & " gh auth login" with title kTitle buttons {"Entendido"} default button "Entendido" with icon stop
93
+ return false
94
+ end try
95
+ -- Usuario de GitHub: gh api /user --jq .login — necesario para armar el check de membresía que sigue
96
+ try
97
+ set ghUser to do shell script "${SHELL:-/bin/zsh} -lc 'gh api /user --jq .login'"
98
+ on error
99
+ display dialog "No pude leer tu usuario de GitHub." & return & return & "Revisa tu sesión con:" & return & " gh auth status" with title kTitle buttons {"Entendido"} default button "Entendido" with icon stop
100
+ return false
101
+ end try
102
+ -- Membresía de la org: mismo check que el CLI (gh api /orgs/<org>/members/<login> → 204 si es miembro)
103
+ try
104
+ do shell script "${SHELL:-/bin/zsh} -lc " & quoted form of ("gh api /orgs/" & kOrg & "/members/" & ghUser)
105
+ on error
106
+ display dialog "Tu cuenta de GitHub (" & ghUser & ") no aparece como miembro de la org " & kOrg & "." & return & return & "Si YA eres miembro y aun así ves esto, a tu sesión le falta un permiso para ver la membresía. Ejecútalo una vez en la terminal:" & return & " gh auth refresh -s read:org" & return & return & "Si NO eres miembro, pide acceso a la org o entra con la cuenta correcta:" & return & " gh auth login" with title kTitle buttons {"Entendido"} default button "Entendido" with icon stop
107
+ return false
108
+ end try
109
+ return true
110
+ end checkPrereqs
111
+
112
+ -- ───────────────────────── Nuevo proyecto ─────────────────────────
113
+ on newProject(homeRoot)
114
+ -- Nombre
115
+ set nameAns to display dialog "¿Cómo se va a llamar el proyecto?" default answer "" with title kTitle buttons {"Cancelar", "Siguiente"} default button "Siguiente"
116
+ if button returned of nameAns is "Cancelar" then return
117
+ set projName to text returned of nameAns
118
+ if projName is "" then
119
+ display alert "Falta el nombre" message "Vuelve a abrir y escribe un nombre para el proyecto." as warning
120
+ return
121
+ end if
122
+
123
+ -- Perfil
124
+ set profChoice to choose from list {"App completa (starter kit + cerebro)", "Solo el cerebro"} with prompt "¿Qué quieres crear para «" & projName & "»?" default items {"App completa (starter kit + cerebro)"} OK button name "Siguiente" cancel button name "Cancelar"
125
+ if profChoice is false then return
126
+ if (item 1 of profChoice) starts with "App completa" then
127
+ set profileFlag to "--full"
128
+ else
129
+ set profileFlag to "--core"
130
+ end if
131
+
132
+ -- Ubicación (default ~/timekast, con opción de elegir otra)
133
+ set locAns to display dialog "¿Dónde guardar el proyecto?" with title kTitle buttons {"Cancelar", "Elegir otra…", "Usar ~/timekast"} default button "Usar ~/timekast"
134
+ set locBtn to button returned of locAns
135
+ if locBtn is "Cancelar" then return
136
+ if locBtn is "Usar ~/timekast" then
137
+ set destFolder to homeRoot
138
+ else
139
+ set destFolder to POSIX path of (choose folder with prompt "Elige dónde crear el proyecto" default location (POSIX file homeRoot))
140
+ -- choose folder devuelve con "/" final; lo normalizamos sin "/" para el join
141
+ if destFolder ends with "/" then set destFolder to text 1 thru -2 of destFolder
142
+ end if
143
+
144
+ -- Crear (oculto, con aviso no-bloqueante)
145
+ display notification "Creando «" & projName & "»… puede tardar ~30 s." with title kTitle
146
+ set innerCmd to "cd " & quoted form of destFolder & " && npx -y " & kCLI & " new " & quoted form of projName & " " & profileFlag
147
+ try
148
+ do shell script "${SHELL:-/bin/zsh} -lc " & quoted form of innerCmd
149
+ on error errMsg number errNum
150
+ display alert "No se pudo crear el proyecto" message errMsg as critical
151
+ return
152
+ end try
153
+
154
+ set projPath to destFolder & "/" & projName
155
+ set doneAns to display dialog "✔ Listo. Tu proyecto está en:" & return & projPath & return & return & "Ábrelo en Claude Code para empezar a trabajar." with title kTitle buttons {"OK", "Abrir carpeta"} default button "Abrir carpeta"
156
+ if button returned of doneAns is "Abrir carpeta" then
157
+ do shell script "open " & quoted form of projPath
158
+ end if
159
+ end newProject
160
+
161
+ -- ──────────────────────── Actualizar proyecto ────────────────────────
162
+ on updateProject(homeRoot)
163
+ set repoFolder to POSIX path of (choose folder with prompt "Elige la carpeta del proyecto a actualizar" default location (POSIX file homeRoot))
164
+ if not (repoFolder ends with "/") then set repoFolder to repoFolder & "/"
165
+
166
+ -- ¿Ya está registrado con el CLI? (tiene lockfile)
167
+ set lockPath to repoFolder & ".timekast/lockfile.json"
168
+ set hasLock to (do shell script "test -f " & quoted form of lockPath & " && echo yes || echo no")
169
+ -- ¿Tiene cerebro instalado a mano? (carpeta .claude sin lockfile = legacy hand-managed).
170
+ -- Solo importa cuando NO hay lockfile. Si no podemos leer el directorio (permisos),
171
+ -- el try cae al caso conservador (hasClaude = "yes" → advertencia + update --yes).
172
+ set hasClaude to "yes"
173
+ try
174
+ set hasClaude to (do shell script "test -d " & quoted form of (repoFolder & ".claude") & " && echo yes || echo no")
175
+ end try
176
+
177
+ -- Tres casos:
178
+ -- · con lockfile → `update` normal (la lógica de 4 cubetas del CLI resuelve conflictos)
179
+ -- · sin lockfile + con .claude → legacy hand-managed: advertencia + `update --yes` (auto-confirma el prompt)
180
+ -- · sin lockfile + sin .claude → repo fresco: `factory add` limpio, sin advertencia
181
+ set cliCmd to "update"
182
+ set updateFlag to ""
183
+ if hasLock is "no" then
184
+ if hasClaude is "yes" then
185
+ set warnAns to display dialog "Este proyecto tiene un cerebro (.claude) instalado a mano y todavía no está registrado con el CLI." & return & return & "Voy a ponerlo al día. Eso SOBRESCRIBE los archivos del cerebro que coincidan con la última versión; tu CLAUDE.md se conserva. Si editaste archivos del cerebro a mano, se reemplazarían." & return & return & "¿Continúo con el registro?" with title kTitle buttons {"Cancelar", "Registrar y actualizar"} default button "Registrar y actualizar" with icon caution
186
+ if button returned of warnAns is "Cancelar" then return
187
+ set updateFlag to " --yes"
188
+ else
189
+ -- Repo fresco: instalación limpia con `add`, sin advertencia de sobrescritura.
190
+ set cliCmd to "add"
191
+ end if
192
+ end if
193
+
194
+ -- Actualizar (oculto)
195
+ display notification "Actualizando el proyecto…" with title kTitle
196
+ set innerCmd to "cd " & quoted form of repoFolder & " && npx -y " & kCLI & " " & cliCmd & updateFlag
197
+ try
198
+ do shell script "${SHELL:-/bin/zsh} -lc " & quoted form of innerCmd
199
+ on error errMsg number errNum
200
+ if errNum is 130 then
201
+ display dialog "Este update tiene cambios que chocan con ediciones tuyas en el cerebro." & return & return & "No se modificó nada. Resuélvelo viendo los diffs en Claude Code, o en terminal con:" & return & " pnpm factory:update" with title kTitle buttons {"Entendido"} default button "Entendido" with icon caution
202
+ else
203
+ display alert "No se pudo actualizar" message errMsg as critical
204
+ end if
205
+ return
206
+ end try
207
+
208
+ display dialog "✔ Proyecto actualizado al día." with title kTitle buttons {"OK"} default button "OK"
209
+ end updateProject
210
+
211
+ -- ──────────────────────────── Poner secrets ────────────────────────────
212
+ -- Instala los secretos de metodología del equipo en ~/.claude/.env llamando
213
+ -- a `npx @timekast/factory secrets <archivo>`. El gate de org (checkPrereqs en
214
+ -- `on run`) ES la autorización: sin membresía no se llega aquí. El comando solo
215
+ -- imprime NOMBRES de clave (nunca valores) y eso es lo que mostramos tal cual.
216
+ on installSecrets()
217
+ -- Elige el archivo master de secrets. Sin filtro de tipo: los .env suelen no
218
+ -- tener extensión. Si el usuario cancela, volvemos al menú sin error.
219
+ try
220
+ set chosenFile to choose file with prompt "Elige tu archivo master de secrets"
221
+ on error number -128
222
+ -- El usuario presionó Cancelar — no es un fallo, simplemente regresamos.
223
+ return
224
+ end try
225
+
226
+ -- `quoted form of POSIX path` maneja rutas con espacios; obligatorio aquí.
227
+ display notification "Instalando secrets…" with title kTitle
228
+ set innerCmd to "npx -y " & kCLI & " secrets " & quoted form of (POSIX path of chosenFile)
229
+ try
230
+ set secretsResult to do shell script "${SHELL:-/bin/zsh} -lc " & quoted form of innerCmd
231
+ on error errMsg number errNum
232
+ display alert "No se pudieron poner los secrets" message errMsg as critical
233
+ return
234
+ end try
235
+
236
+ -- El stdout del CLI ya lista solo NOMBRES de clave (agregadas / actualizadas /
237
+ -- sin cambios), nunca valores — lo mostramos verbatim.
238
+ display dialog secretsResult with title kTitle buttons {"OK"} default button "OK"
239
+ end installSecrets
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@timekast/factory",
3
- "version": "1.10.1",
3
+ "version": "1.11.0",
4
4
  "description": "Public, thin CLI to bootstrap and maintain TimeKast Factory derived projects.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -30,7 +30,7 @@
30
30
  "node": ">=22"
31
31
  },
32
32
  "scripts": {
33
- "build": "tsc -p tsconfig.json",
33
+ "build": "tsc -p tsconfig.json && cp launchers/src/launcher.applescript dist/",
34
34
  "typecheck": "tsc -p tsconfig.json --noEmit",
35
35
  "prepublishOnly": "pnpm build",
36
36
  "test": "vitest run",