@dinoreic/fez 0.4.0 → 0.5.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 +723 -198
- package/bin/fez +16 -6
- package/bin/fez-compile +347 -0
- package/bin/fez-debug +25 -0
- package/bin/fez-index +16 -4
- package/bin/refactor +699 -0
- package/dist/fez.js +142 -33
- package/dist/fez.js.map +4 -4
- package/fez.d.ts +533 -0
- package/package.json +25 -15
- package/src/fez/compile.js +396 -164
- package/src/fez/connect.js +250 -143
- package/src/fez/defaults.js +275 -84
- package/src/fez/instance.js +673 -514
- package/src/fez/lib/await-helper.js +64 -0
- package/src/fez/lib/global-state.js +22 -4
- package/src/fez/lib/index.js +140 -0
- package/src/fez/lib/localstorage.js +44 -0
- package/src/fez/lib/n.js +38 -23
- package/src/fez/lib/pubsub.js +208 -0
- package/src/fez/lib/svelte-template-lib.js +339 -0
- package/src/fez/lib/svelte-template.js +472 -0
- package/src/fez/lib/template.js +114 -119
- package/src/fez/morph.js +384 -0
- package/src/fez/root.js +284 -164
- package/src/fez/utility.js +319 -149
- package/src/fez/utils/dump.js +114 -84
- package/src/fez/utils/highlight_all.js +1 -1
- package/src/fez.js +65 -43
- package/src/rollup.js +1 -1
- package/src/svelte-cde-adapter.coffee +21 -12
- package/src/fez/vendor/idiomorph.js +0 -860
package/bin/fez
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
require 'pathname'
|
|
4
4
|
|
|
5
|
+
# Resolve the actual bin directory (handles symlinks from bunx/npx)
|
|
6
|
+
bin_dir = Pathname.new(__FILE__).realpath.dirname
|
|
7
|
+
|
|
5
8
|
command = ARGV[0]
|
|
6
9
|
args = ARGV[1..-1]
|
|
7
10
|
|
|
@@ -10,22 +13,29 @@ if command.nil? || command == '--help' || command == '-h'
|
|
|
10
13
|
puts ""
|
|
11
14
|
puts "Available commands:"
|
|
12
15
|
|
|
13
|
-
|
|
14
|
-
commands = Dir[bin_dir.join("fez-*")].map do |path|
|
|
16
|
+
subcommands = Dir[bin_dir.join("fez-*")].map do |path|
|
|
15
17
|
File.basename(path).sub(/^fez-/, '')
|
|
16
18
|
end.sort
|
|
17
19
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
+
max_len = subcommands.map(&:length).max || 0
|
|
21
|
+
|
|
22
|
+
subcommands.each do |cmd|
|
|
23
|
+
cmd_path = bin_dir.join("fez-#{cmd}")
|
|
24
|
+
info = `#{cmd_path} --info 2>/dev/null`.strip
|
|
25
|
+
if info.empty?
|
|
26
|
+
puts " #{cmd}"
|
|
27
|
+
else
|
|
28
|
+
puts " #{cmd.ljust(max_len)} #{info}"
|
|
29
|
+
end
|
|
20
30
|
end
|
|
21
31
|
|
|
22
32
|
exit 0
|
|
23
33
|
end
|
|
24
34
|
|
|
25
|
-
subcommand_path =
|
|
35
|
+
subcommand_path = bin_dir.join("fez-#{command}")
|
|
26
36
|
|
|
27
37
|
if File.exist?(subcommand_path) && File.executable?(subcommand_path)
|
|
28
|
-
exec(subcommand_path, *args)
|
|
38
|
+
exec(subcommand_path.to_s, *args)
|
|
29
39
|
else
|
|
30
40
|
puts "fez: '#{command}' is not a fez command. See 'fez --help'."
|
|
31
41
|
exit 1
|
package/bin/fez-compile
ADDED
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { parseArgs } from 'util'
|
|
4
|
+
import fs from 'fs'
|
|
5
|
+
import path from 'path'
|
|
6
|
+
import prettier from 'prettier'
|
|
7
|
+
|
|
8
|
+
const INFO = 'Compile Fez components and report errors'
|
|
9
|
+
|
|
10
|
+
const { values, positionals } = parseArgs({
|
|
11
|
+
args: Bun.argv.slice(2),
|
|
12
|
+
options: {
|
|
13
|
+
help: { type: 'boolean', short: 'h' },
|
|
14
|
+
output: { type: 'boolean', short: 'o' },
|
|
15
|
+
info: { type: 'boolean' },
|
|
16
|
+
},
|
|
17
|
+
allowPositionals: true,
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
if (values.info) {
|
|
21
|
+
console.log(INFO)
|
|
22
|
+
process.exit(0)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (values.help || positionals.length === 0) {
|
|
26
|
+
console.log(`Usage: fez compile <file.fez>
|
|
27
|
+
|
|
28
|
+
${INFO}
|
|
29
|
+
|
|
30
|
+
Options:
|
|
31
|
+
-h, --help Show this help message
|
|
32
|
+
-o, --output Output compiled JavaScript
|
|
33
|
+
|
|
34
|
+
Examples:
|
|
35
|
+
fez compile demo/fez/ui-counter.fez
|
|
36
|
+
fez compile -o demo/fez/ui-counter.fez > compiled.js
|
|
37
|
+
`)
|
|
38
|
+
process.exit(0)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (positionals.length > 1) {
|
|
42
|
+
console.error('Error: Only one file can be compiled at a time')
|
|
43
|
+
process.exit(1)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Parse a .fez file into its components
|
|
47
|
+
function parseFezFile(content) {
|
|
48
|
+
const result = { script: '', style: '', html: '', head: '', demo: '', info: '' }
|
|
49
|
+
const lines = content.split('\n')
|
|
50
|
+
|
|
51
|
+
let currentBlock = []
|
|
52
|
+
let currentType = ''
|
|
53
|
+
|
|
54
|
+
for (let line of lines) {
|
|
55
|
+
const trimmedLine = line.trim()
|
|
56
|
+
|
|
57
|
+
// Start blocks - demo/info can contain other tags, so skip nested detection
|
|
58
|
+
if (trimmedLine.startsWith('<demo') && !result.demo && !currentType) {
|
|
59
|
+
currentType = 'demo'
|
|
60
|
+
} else if (trimmedLine.startsWith('<info') && !result.info && !currentType) {
|
|
61
|
+
currentType = 'info'
|
|
62
|
+
} else if (trimmedLine.startsWith('<script') && !result.script && currentType !== 'head' && currentType !== 'demo' && currentType !== 'info') {
|
|
63
|
+
currentType = 'script'
|
|
64
|
+
} else if (trimmedLine.startsWith('<head') && !result.script && currentType !== 'demo' && currentType !== 'info') {
|
|
65
|
+
currentType = 'head'
|
|
66
|
+
} else if (trimmedLine.startsWith('<style') && currentType !== 'demo' && currentType !== 'info') {
|
|
67
|
+
currentType = 'style'
|
|
68
|
+
} else if (trimmedLine.endsWith('</demo>') && currentType === 'demo') {
|
|
69
|
+
result.demo = currentBlock.join('\n')
|
|
70
|
+
currentBlock = []
|
|
71
|
+
currentType = ''
|
|
72
|
+
} else if (trimmedLine.endsWith('</info>') && currentType === 'info') {
|
|
73
|
+
result.info = currentBlock.join('\n')
|
|
74
|
+
currentBlock = []
|
|
75
|
+
currentType = ''
|
|
76
|
+
} else if (trimmedLine.endsWith('</script>') && currentType === 'script' && !result.script) {
|
|
77
|
+
result.script = currentBlock.join('\n')
|
|
78
|
+
currentBlock = []
|
|
79
|
+
currentType = ''
|
|
80
|
+
} else if (trimmedLine.endsWith('</style>') && currentType === 'style') {
|
|
81
|
+
result.style = currentBlock.join('\n')
|
|
82
|
+
currentBlock = []
|
|
83
|
+
currentType = ''
|
|
84
|
+
} else if ((trimmedLine.endsWith('</head>') || trimmedLine.endsWith('</header>')) && currentType === 'head') {
|
|
85
|
+
result.head = currentBlock.join('\n')
|
|
86
|
+
currentBlock = []
|
|
87
|
+
currentType = ''
|
|
88
|
+
} else if (currentType) {
|
|
89
|
+
currentBlock.push(currentType === 'demo' || currentType === 'info' ? line : trimmedLine)
|
|
90
|
+
} else {
|
|
91
|
+
result.html += line + '\n'
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return result
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Wrap script content in a class if not already wrapped
|
|
99
|
+
function wrapInClass(script) {
|
|
100
|
+
if (/class\s+\{/.test(script)) {
|
|
101
|
+
return script
|
|
102
|
+
}
|
|
103
|
+
return `class {\n${script}\n}`
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Validate JavaScript syntax
|
|
107
|
+
function validateScript(script, filePath) {
|
|
108
|
+
const errors = []
|
|
109
|
+
|
|
110
|
+
if (!script.trim()) {
|
|
111
|
+
return errors
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Check for ES module imports
|
|
115
|
+
const hasImports = /^\s*import\s+/m.test(script)
|
|
116
|
+
|
|
117
|
+
// Check if script has explicit class { } declaration
|
|
118
|
+
const hasExplicitClass = /class\s+\{/.test(script)
|
|
119
|
+
|
|
120
|
+
if (hasExplicitClass) {
|
|
121
|
+
// Split into parts: imports/top-level code and class
|
|
122
|
+
let parts = script.split(/class\s+\{/, 2)
|
|
123
|
+
let preamble = parts[0] || ''
|
|
124
|
+
let classBody = parts[1] ? `class {\n${parts[1]}` : ''
|
|
125
|
+
|
|
126
|
+
// Validate preamble (imports, const declarations)
|
|
127
|
+
if (preamble.trim()) {
|
|
128
|
+
// Remove import statements for validation (they're valid but can't be validated with new Function)
|
|
129
|
+
let preambleWithoutImports = preamble.replace(/^\s*import\s+.*$/gm, '// import removed')
|
|
130
|
+
|
|
131
|
+
if (preambleWithoutImports.trim()) {
|
|
132
|
+
try {
|
|
133
|
+
new Function(preambleWithoutImports)
|
|
134
|
+
} catch (e) {
|
|
135
|
+
if (!hasImports || !e.message.includes('import')) {
|
|
136
|
+
const lineMatch = e.stack?.match(/<anonymous>:(\d+):(\d+)/)
|
|
137
|
+
let errorInfo = { message: e.message, file: filePath, kind: 'JavaScript' }
|
|
138
|
+
if (lineMatch) {
|
|
139
|
+
errorInfo.line = parseInt(lineMatch[1])
|
|
140
|
+
errorInfo.column = parseInt(lineMatch[2])
|
|
141
|
+
}
|
|
142
|
+
errors.push(errorInfo)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Validate class body
|
|
149
|
+
if (classBody.trim()) {
|
|
150
|
+
const code = `(${classBody})`
|
|
151
|
+
try {
|
|
152
|
+
new Function(code)
|
|
153
|
+
} catch (e) {
|
|
154
|
+
const lineMatch = e.stack?.match(/<anonymous>:(\d+):(\d+)/)
|
|
155
|
+
let errorInfo = { message: e.message, file: filePath, kind: 'JavaScript' }
|
|
156
|
+
if (lineMatch) {
|
|
157
|
+
// Adjust for preamble lines
|
|
158
|
+
const preambleLines = preamble.split('\n').length
|
|
159
|
+
errorInfo.line = parseInt(lineMatch[1]) + preambleLines - 2
|
|
160
|
+
errorInfo.column = parseInt(lineMatch[2])
|
|
161
|
+
}
|
|
162
|
+
errors.push(errorInfo)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
} else {
|
|
166
|
+
// No explicit class - wrap entire script in class
|
|
167
|
+
let klass = wrapInClass(script)
|
|
168
|
+
const code = `(${klass})`
|
|
169
|
+
try {
|
|
170
|
+
new Function(code)
|
|
171
|
+
} catch (e) {
|
|
172
|
+
const lineMatch = e.stack?.match(/<anonymous>:(\d+):(\d+)/)
|
|
173
|
+
let errorInfo = { message: e.message, file: filePath, kind: 'JavaScript' }
|
|
174
|
+
if (lineMatch) {
|
|
175
|
+
errorInfo.line = parseInt(lineMatch[1]) - 2
|
|
176
|
+
errorInfo.column = parseInt(lineMatch[2])
|
|
177
|
+
}
|
|
178
|
+
errors.push(errorInfo)
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return errors
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Validate template syntax (basic checks)
|
|
186
|
+
function validateTemplate(html, filePath) {
|
|
187
|
+
const errors = []
|
|
188
|
+
|
|
189
|
+
// Count both {{if and {{#if variants
|
|
190
|
+
const ifOpens = (html.match(/\{\{#?if\s/g) || []).length
|
|
191
|
+
const ifCloses = (html.match(/\{\{\/?#?if\}\}/g) || []).length
|
|
192
|
+
if (ifOpens !== ifCloses) {
|
|
193
|
+
errors.push({
|
|
194
|
+
message: `Unmatched {{if}} blocks: ${ifOpens} opens, ${ifCloses} closes`,
|
|
195
|
+
file: filePath,
|
|
196
|
+
kind: 'Template'
|
|
197
|
+
})
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Check for unmatched {{for}}/{{/for}}
|
|
201
|
+
const forOpens = (html.match(/\{\{for\s/g) || []).length
|
|
202
|
+
const forCloses = (html.match(/\{\{\/for\}\}/g) || []).length
|
|
203
|
+
if (forOpens !== forCloses) {
|
|
204
|
+
errors.push({
|
|
205
|
+
message: `Unmatched {{for}} blocks: ${forOpens} opens, ${forCloses} closes`,
|
|
206
|
+
file: filePath,
|
|
207
|
+
kind: 'Template'
|
|
208
|
+
})
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Check for {{if}} inside attributes (common mistake)
|
|
212
|
+
const attrIfMatch = html.match(/\w+=["'][^"']*\{\{#?if\s/g)
|
|
213
|
+
if (attrIfMatch) {
|
|
214
|
+
errors.push({
|
|
215
|
+
message: `{{if}} block found inside attribute - use ternary operator instead`,
|
|
216
|
+
file: filePath,
|
|
217
|
+
kind: 'Template'
|
|
218
|
+
})
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return errors
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Generate compiled JavaScript from parsed .fez file
|
|
225
|
+
function generateCompiledJS(parsed, componentName) {
|
|
226
|
+
let klass = parsed.script
|
|
227
|
+
|
|
228
|
+
if (!/class\s+\{/.test(klass)) {
|
|
229
|
+
klass = `class {\n${klass}\n}`
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Add CSS if present
|
|
233
|
+
if (parsed.style && parsed.style.includes(':')) {
|
|
234
|
+
let style = parsed.style
|
|
235
|
+
// Wrap in :fez if not already scoped
|
|
236
|
+
if (!style.includes(':fez') && !/(?:^|\s)body\s*\{/.test(style)) {
|
|
237
|
+
style = `:fez {\n${style}\n}`
|
|
238
|
+
}
|
|
239
|
+
klass = klass.replace(/\}\s*$/, `\n CSS = \`${style}\`\n}`)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Add HTML if present
|
|
243
|
+
if (parsed.html && /\w/.test(parsed.html)) {
|
|
244
|
+
// Escape backticks and dollar signs in template, trim whitespace
|
|
245
|
+
let html = parsed.html
|
|
246
|
+
.trim()
|
|
247
|
+
.replaceAll('`', '`')
|
|
248
|
+
.replaceAll('$', '\\$')
|
|
249
|
+
klass = klass.replace(/\}\s*$/, `\n HTML = \`${html}\`\n}`)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Split into preamble (imports) and class body
|
|
253
|
+
let parts = klass.split(/class\s+\{/, 2)
|
|
254
|
+
let preamble = parts[0] || ''
|
|
255
|
+
let classBody = parts[1] ? `class {\n${parts[1]}` : klass
|
|
256
|
+
|
|
257
|
+
// Build final output
|
|
258
|
+
let output = ''
|
|
259
|
+
if (preamble.trim()) {
|
|
260
|
+
output += preamble.trim() + '\n\n'
|
|
261
|
+
}
|
|
262
|
+
output += `Fez('${componentName}', ${classBody})`
|
|
263
|
+
|
|
264
|
+
return output
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async function formatCompiledJS(code, filePath) {
|
|
268
|
+
try {
|
|
269
|
+
return { code: await prettier.format(code, { parser: 'babel' }) }
|
|
270
|
+
} catch (error) {
|
|
271
|
+
return {
|
|
272
|
+
error: {
|
|
273
|
+
message: error?.message || 'Failed to format compiled JavaScript',
|
|
274
|
+
file: filePath,
|
|
275
|
+
kind: 'Format',
|
|
276
|
+
},
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Process a single file
|
|
282
|
+
async function compileFile(filePath) {
|
|
283
|
+
const errors = []
|
|
284
|
+
const absolutePath = path.resolve(filePath)
|
|
285
|
+
|
|
286
|
+
if (!fs.existsSync(absolutePath)) {
|
|
287
|
+
return {
|
|
288
|
+
errors: [{ message: `File not found: ${filePath}`, file: filePath, kind: 'File' }],
|
|
289
|
+
compiled: null,
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const content = fs.readFileSync(absolutePath, 'utf-8')
|
|
294
|
+
const componentName = path.basename(filePath, '.fez')
|
|
295
|
+
|
|
296
|
+
// Validate component name has a dash
|
|
297
|
+
if (!componentName.includes('-')) {
|
|
298
|
+
errors.push({
|
|
299
|
+
message: `Invalid component name "${componentName}". Custom element names must contain a dash (e.g., 'my-element', 'ui-button').`,
|
|
300
|
+
file: filePath,
|
|
301
|
+
kind: 'Naming'
|
|
302
|
+
})
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const parsed = parseFezFile(content)
|
|
306
|
+
|
|
307
|
+
// Validate script
|
|
308
|
+
errors.push(...validateScript(parsed.script, filePath))
|
|
309
|
+
|
|
310
|
+
// Validate template
|
|
311
|
+
errors.push(...validateTemplate(parsed.html, filePath))
|
|
312
|
+
|
|
313
|
+
// Generate compiled output only if no errors
|
|
314
|
+
let compiled = errors.length === 0 ? generateCompiledJS(parsed, componentName) : null
|
|
315
|
+
if (compiled) {
|
|
316
|
+
const formatted = await formatCompiledJS(compiled, filePath)
|
|
317
|
+
if (formatted.error) {
|
|
318
|
+
errors.push(formatted.error)
|
|
319
|
+
compiled = null
|
|
320
|
+
} else {
|
|
321
|
+
compiled = formatted.code
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return { errors, compiled }
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Main execution
|
|
329
|
+
const filePath = positionals[0]
|
|
330
|
+
const { errors, compiled } = await compileFile(filePath)
|
|
331
|
+
|
|
332
|
+
if (errors.length > 0) {
|
|
333
|
+
for (const error of errors) {
|
|
334
|
+
const location = error.line ? `:${error.line}${error.column ? ':' + error.column : ''}` : ''
|
|
335
|
+
const kind = error.kind ? `${error.kind} error: ` : ''
|
|
336
|
+
console.error(`${error.file}${location}: ${kind}${error.message}`)
|
|
337
|
+
}
|
|
338
|
+
process.exit(1)
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (values.output && compiled) {
|
|
342
|
+
console.log(compiled)
|
|
343
|
+
} else {
|
|
344
|
+
console.log('Compiled without errors, add -o to show output')
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
process.exit(0)
|
package/bin/fez-debug
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
if ARGV[0] == '--info'
|
|
4
|
+
puts "Debug URL with Playwright (for LLM agents)"
|
|
5
|
+
exit 0
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
url = ARGV[0]
|
|
9
|
+
|
|
10
|
+
if url.nil? || url == '--help' || url == '-h'
|
|
11
|
+
puts "Usage: fez debug <url>"
|
|
12
|
+
puts ""
|
|
13
|
+
puts "Opens a Playwright browser session for LLM agents to debug web pages."
|
|
14
|
+
puts "The page object is exposed globally for programmatic inspection."
|
|
15
|
+
puts ""
|
|
16
|
+
puts "Example:"
|
|
17
|
+
puts " fez debug http://localhost:3333"
|
|
18
|
+
exit 0
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
bin_dir = File.dirname(File.realpath(__FILE__))
|
|
22
|
+
project_dir = File.dirname(bin_dir)
|
|
23
|
+
script_path = File.join(project_dir, 'bun', 'playwright-debug.js')
|
|
24
|
+
|
|
25
|
+
exec('bun', script_path, url)
|
package/bin/fez-index
CHANGED
|
@@ -3,10 +3,22 @@
|
|
|
3
3
|
require 'pathname'
|
|
4
4
|
require 'json'
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
INFO = 'Generate JSON index of files in a directory'
|
|
7
|
+
|
|
8
|
+
if ARGV[0] == '--info'
|
|
9
|
+
puts INFO
|
|
10
|
+
exit 0
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
if ARGV.empty? || ARGV[0] == '-h' || ARGV[0] == '--help'
|
|
14
|
+
puts "Usage: fez index <directory>"
|
|
15
|
+
puts ""
|
|
16
|
+
puts INFO
|
|
17
|
+
puts ""
|
|
18
|
+
puts "Examples:"
|
|
19
|
+
puts " fez index demo/fez"
|
|
20
|
+
puts " fez index assets/*"
|
|
21
|
+
exit 0
|
|
10
22
|
end
|
|
11
23
|
|
|
12
24
|
dir = ARGV[0]
|